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.
Files changed (382) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build_deploy.yml +59 -0
  3. data/.github/workflows/links.yml +85 -0
  4. data/.github/workflows/rake.yml +15 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +68 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +14 -0
  9. data/.rubocop_todo.yml +70 -0
  10. data/ARCHITECTURE.md +744 -0
  11. data/Gemfile +12 -0
  12. data/LICENSE +25 -0
  13. data/README.adoc +357 -0
  14. data/Rakefile +11 -0
  15. data/docs/.gitignore +1 -0
  16. data/docs/Gemfile +13 -0
  17. data/docs/_config.yml +182 -0
  18. data/docs/_diagram_types/architecture-diagram.adoc +314 -0
  19. data/docs/_diagram_types/block-diagram.adoc +345 -0
  20. data/docs/_diagram_types/c4-diagram.adoc +559 -0
  21. data/docs/_diagram_types/class-diagram.adoc +816 -0
  22. data/docs/_diagram_types/er-diagram.adoc +719 -0
  23. data/docs/_diagram_types/error-diagram.adoc +114 -0
  24. data/docs/_diagram_types/examples/flowchart-examples.adoc +29 -0
  25. data/docs/_diagram_types/flowchart.adoc +488 -0
  26. data/docs/_diagram_types/gantt-chart.adoc +502 -0
  27. data/docs/_diagram_types/git-graph.adoc +600 -0
  28. data/docs/_diagram_types/index.adoc +192 -0
  29. data/docs/_diagram_types/info-diagram.adoc +103 -0
  30. data/docs/_diagram_types/kanban-diagram.adoc +262 -0
  31. data/docs/_diagram_types/mindmap.adoc +603 -0
  32. data/docs/_diagram_types/packet-diagram.adoc +378 -0
  33. data/docs/_diagram_types/pie-chart.adoc +335 -0
  34. data/docs/_diagram_types/quadrant-chart.adoc +406 -0
  35. data/docs/_diagram_types/radar-chart.adoc +528 -0
  36. data/docs/_diagram_types/requirement-diagram.adoc +416 -0
  37. data/docs/_diagram_types/sankey-diagram.adoc +357 -0
  38. data/docs/_diagram_types/sequence-diagram.adoc +664 -0
  39. data/docs/_diagram_types/state-diagram.adoc +658 -0
  40. data/docs/_diagram_types/timeline.adoc +352 -0
  41. data/docs/_diagram_types/treemap-diagram.adoc +462 -0
  42. data/docs/_diagram_types/user-journey.adoc +602 -0
  43. data/docs/_features/index.adoc +129 -0
  44. data/docs/_guides/cli-reference.adoc +203 -0
  45. data/docs/_guides/index.adoc +56 -0
  46. data/docs/_guides/installation.adoc +100 -0
  47. data/docs/_guides/quick-start.adoc +132 -0
  48. data/docs/_pages/comparison.adoc +441 -0
  49. data/docs/_pages/compatibility.adoc +300 -0
  50. data/docs/_pages/index.adoc +39 -0
  51. data/docs/_references/index.adoc +103 -0
  52. data/docs/_tutorials/index.adoc +57 -0
  53. data/docs/index.adoc +166 -0
  54. data/docs/lychee.toml +54 -0
  55. data/examples/.gitignore +10 -0
  56. data/examples/README.adoc +196 -0
  57. data/examples/README.md +64 -0
  58. data/examples/architecture/01-basic-services.mmd +9 -0
  59. data/examples/architecture/01-basic-services.svg +37 -0
  60. data/examples/architecture/02-service-groups.mmd +16 -0
  61. data/examples/architecture/02-service-groups.svg +55 -0
  62. data/examples/architecture/README.adoc +79 -0
  63. data/examples/block/01-basic-blocks.mmd +13 -0
  64. data/examples/block/01-basic-blocks.svg +44 -0
  65. data/examples/block/02-block-shapes.mmd +13 -0
  66. data/examples/block/02-block-shapes.svg +47 -0
  67. data/examples/block/README.adoc +85 -0
  68. data/examples/c4/01-context-diagram.mmd +10 -0
  69. data/examples/c4/01-context-diagram.svg +45 -0
  70. data/examples/c4/02-container-diagram.mmd +24 -0
  71. data/examples/c4/02-container-diagram.svg +105 -0
  72. data/examples/c4/README.adoc +92 -0
  73. data/examples/class_diagram/01-basic-classes.mmd +61 -0
  74. data/examples/class_diagram/01-basic-classes.svg +117 -0
  75. data/examples/class_diagram/02-relationships.mmd +61 -0
  76. data/examples/class_diagram/02-relationships.svg +129 -0
  77. data/examples/class_diagram/README.adoc +93 -0
  78. data/examples/er_diagram/01-basic-entities.mmd +64 -0
  79. data/examples/er_diagram/01-basic-entities.svg +5 -0
  80. data/examples/er_diagram/02-cardinality.mmd +57 -0
  81. data/examples/er_diagram/02-cardinality.svg +125 -0
  82. data/examples/er_diagram/README.adoc +88 -0
  83. data/examples/error/01-basic-error.mmd +1 -0
  84. data/examples/error/01-basic-error.svg +13 -0
  85. data/examples/error/02-error-display.mmd +1 -0
  86. data/examples/error/02-error-display.svg +13 -0
  87. data/examples/error/README.adoc +71 -0
  88. data/examples/error_message_example.svg +13 -0
  89. data/examples/flowchart/00-original.mmd +13 -0
  90. data/examples/flowchart/00-original.svg +5 -0
  91. data/examples/flowchart/01-basic-flow.mmd +7 -0
  92. data/examples/flowchart/01-basic-flow.svg +52 -0
  93. data/examples/flowchart/01-basic-flow.yml +13 -0
  94. data/examples/flowchart/02*.svg +87 -0
  95. data/examples/flowchart/02-node-shapes.mmd +9 -0
  96. data/examples/flowchart/02-node-shapes.svg +33 -0
  97. data/examples/flowchart/03-edge-types.mmd +7 -0
  98. data/examples/flowchart/03-edge-types.svg +53 -0
  99. data/examples/flowchart/04-subgraphs.mmd +9 -0
  100. data/examples/flowchart/04-subgraphs.svg +33 -0
  101. data/examples/flowchart/05-styling.mmd +9 -0
  102. data/examples/flowchart/05-styling.svg +33 -0
  103. data/examples/flowchart/06-complex-flow.mmd +8 -0
  104. data/examples/flowchart/06-complex-flow.svg +59 -0
  105. data/examples/flowchart/README.adoc +167 -0
  106. data/examples/gantt/01-simple-timeline.* +14 -0
  107. data/examples/gantt/01-simple-timeline.mmd +6 -0
  108. data/examples/gantt/01-simple-timeline.svg +26 -0
  109. data/examples/gantt/02-task-dependencies.mmd +6 -0
  110. data/examples/gantt/02-task-dependencies.svg +26 -0
  111. data/examples/gantt/README.adoc +86 -0
  112. data/examples/git_graph/01-linear-history.mmd +12 -0
  113. data/examples/git_graph/01-linear-history.svg +26 -0
  114. data/examples/git_graph/02-branching.mmd +12 -0
  115. data/examples/git_graph/02-branching.svg +26 -0
  116. data/examples/git_graph/README.adoc +73 -0
  117. data/examples/info/02-showinfo.mmd +1 -0
  118. data/examples/info/02-showinfo.svg +10 -0
  119. data/examples/info/README.adoc +58 -0
  120. data/examples/info_showinfo_example.svg +10 -0
  121. data/examples/kanban/01-simple-board.mmd +8 -0
  122. data/examples/kanban/01-simple-board.svg +43 -0
  123. data/examples/kanban/02-workflow.mmd +8 -0
  124. data/examples/kanban/02-workflow.svg +43 -0
  125. data/examples/kanban/README.adoc +79 -0
  126. data/examples/mindmap/01-simple-tree.mmd +19 -0
  127. data/examples/mindmap/01-simple-tree.svg +61 -0
  128. data/examples/mindmap/02-knowledge-map.mmd +19 -0
  129. data/examples/mindmap/02-knowledge-map.svg +61 -0
  130. data/examples/mindmap/README.adoc +77 -0
  131. data/examples/packet/01-basic-packet.* +17 -0
  132. data/examples/packet/01-basic-packet.mmd +4 -0
  133. data/examples/packet/01-basic-packet.svg +82 -0
  134. data/examples/packet/README.adoc +58 -0
  135. data/examples/pie/01-simple-chart.mmd +5 -0
  136. data/examples/pie/01-simple-chart.svg +17 -0
  137. data/examples/pie/02-labeled-slices.mmd +6 -0
  138. data/examples/pie/02-labeled-slices.svg +19 -0
  139. data/examples/pie/README.adoc +75 -0
  140. data/examples/quadrant/01-basic-quadrant.mmd +13 -0
  141. data/examples/quadrant/01-basic-quadrant.svg +33 -0
  142. data/examples/quadrant/02-positioned-items.mmd +14 -0
  143. data/examples/quadrant/02-positioned-items.svg +35 -0
  144. data/examples/quadrant/README.adoc +84 -0
  145. data/examples/radar/01-simple-radar.* +5 -0
  146. data/examples/radar/01-simple-radar.mmd +3 -0
  147. data/examples/radar/01-simple-radar.svg +25 -0
  148. data/examples/radar/02-multiple-curves.mmd +4 -0
  149. data/examples/radar/02-multiple-curves.svg +43 -0
  150. data/examples/radar/README.adoc +75 -0
  151. data/examples/requirement/01-basic-requirements.mmd +23 -0
  152. data/examples/requirement/01-basic-requirements.svg +49 -0
  153. data/examples/requirement/02-risk-levels.mmd +23 -0
  154. data/examples/requirement/02-risk-levels.svg +49 -0
  155. data/examples/requirement/README.adoc +85 -0
  156. data/examples/sankey/01-simple-flow.mmd +7 -0
  157. data/examples/sankey/01-simple-flow.svg +34 -0
  158. data/examples/sankey/02-multi-stage.mmd +11 -0
  159. data/examples/sankey/02-multi-stage.svg +44 -0
  160. data/examples/sankey/README.adoc +74 -0
  161. data/examples/sequence/01-basic-sequence.mmd +27 -0
  162. data/examples/sequence/01-basic-sequence.svg +5 -0
  163. data/examples/sequence/02-activations.mmd +17 -0
  164. data/examples/sequence/02-activations.svg +78 -0
  165. data/examples/sequence/README.adoc +86 -0
  166. data/examples/state_diagram/01-simple-states.mmd +29 -0
  167. data/examples/state_diagram/01-simple-states.svg +5 -0
  168. data/examples/state_diagram/02-composite.mmd +19 -0
  169. data/examples/state_diagram/02-composite.svg +81 -0
  170. data/examples/state_diagram/README.adoc +90 -0
  171. data/examples/timeline/01-simple-timeline.mmd +11 -0
  172. data/examples/timeline/01-simple-timeline.svg +36 -0
  173. data/examples/timeline/02-periods.mmd +15 -0
  174. data/examples/timeline/02-periods.svg +47 -0
  175. data/examples/timeline/README.adoc +78 -0
  176. data/examples/treemap/01-basic-treemap.mmd +12 -0
  177. data/examples/treemap/01-basic-treemap.svg +59 -0
  178. data/examples/treemap/README.adoc +59 -0
  179. data/examples/user_journey/01-simple-journey.mmd +23 -0
  180. data/examples/user_journey/01-simple-journey.svg +5 -0
  181. data/examples/user_journey/02-multi-actor.mmd +18 -0
  182. data/examples/user_journey/02-multi-actor.svg +129 -0
  183. data/examples/user_journey/README.adoc +81 -0
  184. data/examples/xychart/01-line-chart.mmd +5 -0
  185. data/examples/xychart/01-line-chart.svg +43 -0
  186. data/examples/xychart/02-bar-chart.mmd +7 -0
  187. data/examples/xychart/02-bar-chart.svg +48 -0
  188. data/examples/xychart/README.adoc +80 -0
  189. data/exe/sirena +7 -0
  190. data/lib/sirena/cli.rb +138 -0
  191. data/lib/sirena/commands/batch.rb +117 -0
  192. data/lib/sirena/commands/render.rb +80 -0
  193. data/lib/sirena/commands/types.rb +29 -0
  194. data/lib/sirena/commands/version.rb +24 -0
  195. data/lib/sirena/diagram/architecture.rb +46 -0
  196. data/lib/sirena/diagram/base.rb +61 -0
  197. data/lib/sirena/diagram/block.rb +81 -0
  198. data/lib/sirena/diagram/c4.rb +328 -0
  199. data/lib/sirena/diagram/class_diagram.rb +385 -0
  200. data/lib/sirena/diagram/er_diagram.rb +238 -0
  201. data/lib/sirena/diagram/error.rb +38 -0
  202. data/lib/sirena/diagram/flowchart.rb +160 -0
  203. data/lib/sirena/diagram/gantt.rb +71 -0
  204. data/lib/sirena/diagram/git_graph.rb +36 -0
  205. data/lib/sirena/diagram/info.rb +38 -0
  206. data/lib/sirena/diagram/kanban.rb +178 -0
  207. data/lib/sirena/diagram/mindmap.rb +54 -0
  208. data/lib/sirena/diagram/packet.rb +79 -0
  209. data/lib/sirena/diagram/pie.rb +115 -0
  210. data/lib/sirena/diagram/quadrant.rb +138 -0
  211. data/lib/sirena/diagram/radar.rb +52 -0
  212. data/lib/sirena/diagram/requirement.rb +133 -0
  213. data/lib/sirena/diagram/sankey.rb +217 -0
  214. data/lib/sirena/diagram/sequence.rb +242 -0
  215. data/lib/sirena/diagram/state_diagram.rb +237 -0
  216. data/lib/sirena/diagram/timeline.rb +171 -0
  217. data/lib/sirena/diagram/treemap.rb +84 -0
  218. data/lib/sirena/diagram/user_journey.rb +149 -0
  219. data/lib/sirena/diagram/xy_chart.rb +76 -0
  220. data/lib/sirena/diagram.rb +8 -0
  221. data/lib/sirena/diagram_registry.rb +101 -0
  222. data/lib/sirena/engine.rb +292 -0
  223. data/lib/sirena/parser/architecture.rb +41 -0
  224. data/lib/sirena/parser/base.rb +41 -0
  225. data/lib/sirena/parser/block.rb +72 -0
  226. data/lib/sirena/parser/c4.rb +53 -0
  227. data/lib/sirena/parser/class_diagram.rb +63 -0
  228. data/lib/sirena/parser/er_diagram.rb +40 -0
  229. data/lib/sirena/parser/error.rb +49 -0
  230. data/lib/sirena/parser/flowchart.rb +71 -0
  231. data/lib/sirena/parser/gantt.rb +60 -0
  232. data/lib/sirena/parser/git_graph.rb +95 -0
  233. data/lib/sirena/parser/grammars/architecture.rb +145 -0
  234. data/lib/sirena/parser/grammars/block.rb +190 -0
  235. data/lib/sirena/parser/grammars/c4.rb +226 -0
  236. data/lib/sirena/parser/grammars/class_diagram.rb +284 -0
  237. data/lib/sirena/parser/grammars/common.rb +84 -0
  238. data/lib/sirena/parser/grammars/er_diagram.rb +114 -0
  239. data/lib/sirena/parser/grammars/error.rb +40 -0
  240. data/lib/sirena/parser/grammars/flowchart.rb +298 -0
  241. data/lib/sirena/parser/grammars/gantt.rb +252 -0
  242. data/lib/sirena/parser/grammars/git_graph.rb +167 -0
  243. data/lib/sirena/parser/grammars/info.rb +58 -0
  244. data/lib/sirena/parser/grammars/kanban.rb +83 -0
  245. data/lib/sirena/parser/grammars/mindmap.rb +115 -0
  246. data/lib/sirena/parser/grammars/packet.rb +73 -0
  247. data/lib/sirena/parser/grammars/pie.rb +128 -0
  248. data/lib/sirena/parser/grammars/quadrant.rb +199 -0
  249. data/lib/sirena/parser/grammars/radar.rb +150 -0
  250. data/lib/sirena/parser/grammars/requirement.rb +188 -0
  251. data/lib/sirena/parser/grammars/sankey.rb +104 -0
  252. data/lib/sirena/parser/grammars/sequence.rb +247 -0
  253. data/lib/sirena/parser/grammars/state_diagram.rb +172 -0
  254. data/lib/sirena/parser/grammars/timeline.rb +142 -0
  255. data/lib/sirena/parser/grammars/treemap.rb +120 -0
  256. data/lib/sirena/parser/grammars/xy_chart.rb +120 -0
  257. data/lib/sirena/parser/info.rb +49 -0
  258. data/lib/sirena/parser/kanban.rb +97 -0
  259. data/lib/sirena/parser/mindmap.rb +106 -0
  260. data/lib/sirena/parser/packet.rb +76 -0
  261. data/lib/sirena/parser/pie.rb +49 -0
  262. data/lib/sirena/parser/quadrant.rb +57 -0
  263. data/lib/sirena/parser/radar.rb +104 -0
  264. data/lib/sirena/parser/requirement.rb +70 -0
  265. data/lib/sirena/parser/sankey.rb +64 -0
  266. data/lib/sirena/parser/sequence.rb +51 -0
  267. data/lib/sirena/parser/state_diagram.rb +69 -0
  268. data/lib/sirena/parser/timeline.rb +57 -0
  269. data/lib/sirena/parser/transforms/architecture.rb +97 -0
  270. data/lib/sirena/parser/transforms/block.rb +254 -0
  271. data/lib/sirena/parser/transforms/c4.rb +347 -0
  272. data/lib/sirena/parser/transforms/class_diagram.rb +352 -0
  273. data/lib/sirena/parser/transforms/er_diagram.rb +169 -0
  274. data/lib/sirena/parser/transforms/error.rb +58 -0
  275. data/lib/sirena/parser/transforms/flowchart.rb +293 -0
  276. data/lib/sirena/parser/transforms/gantt.rb +215 -0
  277. data/lib/sirena/parser/transforms/git_graph.rb +160 -0
  278. data/lib/sirena/parser/transforms/info.rb +58 -0
  279. data/lib/sirena/parser/transforms/kanban.rb +176 -0
  280. data/lib/sirena/parser/transforms/mindmap.rb +227 -0
  281. data/lib/sirena/parser/transforms/packet.rb +63 -0
  282. data/lib/sirena/parser/transforms/pie.rb +143 -0
  283. data/lib/sirena/parser/transforms/quadrant.rb +177 -0
  284. data/lib/sirena/parser/transforms/radar.rb +126 -0
  285. data/lib/sirena/parser/transforms/requirement.rb +272 -0
  286. data/lib/sirena/parser/transforms/sankey.rb +122 -0
  287. data/lib/sirena/parser/transforms/sequence.rb +342 -0
  288. data/lib/sirena/parser/transforms/state_diagram.rb +292 -0
  289. data/lib/sirena/parser/transforms/timeline.rb +177 -0
  290. data/lib/sirena/parser/transforms/treemap.rb +81 -0
  291. data/lib/sirena/parser/transforms/xy_chart.rb +132 -0
  292. data/lib/sirena/parser/treemap.rb +98 -0
  293. data/lib/sirena/parser/user_journey.rb +120 -0
  294. data/lib/sirena/parser/xy_chart.rb +114 -0
  295. data/lib/sirena/parser.rb +8 -0
  296. data/lib/sirena/renderer/architecture.rb +251 -0
  297. data/lib/sirena/renderer/base.rb +251 -0
  298. data/lib/sirena/renderer/block.rb +286 -0
  299. data/lib/sirena/renderer/c4.rb +490 -0
  300. data/lib/sirena/renderer/class_diagram.rb +499 -0
  301. data/lib/sirena/renderer/er_diagram.rb +417 -0
  302. data/lib/sirena/renderer/error.rb +131 -0
  303. data/lib/sirena/renderer/flowchart.rb +301 -0
  304. data/lib/sirena/renderer/gantt.rb +331 -0
  305. data/lib/sirena/renderer/git_graph.rb +368 -0
  306. data/lib/sirena/renderer/info.rb +93 -0
  307. data/lib/sirena/renderer/kanban.rb +295 -0
  308. data/lib/sirena/renderer/mindmap.rb +396 -0
  309. data/lib/sirena/renderer/packet.rb +239 -0
  310. data/lib/sirena/renderer/pie.rb +235 -0
  311. data/lib/sirena/renderer/quadrant.rb +292 -0
  312. data/lib/sirena/renderer/radar.rb +323 -0
  313. data/lib/sirena/renderer/requirement.rb +371 -0
  314. data/lib/sirena/renderer/sankey.rb +255 -0
  315. data/lib/sirena/renderer/sequence.rb +424 -0
  316. data/lib/sirena/renderer/state_diagram.rb +328 -0
  317. data/lib/sirena/renderer/timeline.rb +304 -0
  318. data/lib/sirena/renderer/treemap.rb +152 -0
  319. data/lib/sirena/renderer/user_journey.rb +331 -0
  320. data/lib/sirena/renderer/xy_chart.rb +452 -0
  321. data/lib/sirena/renderer.rb +8 -0
  322. data/lib/sirena/svg/circle.rb +41 -0
  323. data/lib/sirena/svg/document.rb +103 -0
  324. data/lib/sirena/svg/element.rb +65 -0
  325. data/lib/sirena/svg/ellipse.rb +33 -0
  326. data/lib/sirena/svg/group.rb +71 -0
  327. data/lib/sirena/svg/line.rb +49 -0
  328. data/lib/sirena/svg/path.rb +76 -0
  329. data/lib/sirena/svg/polygon.rb +43 -0
  330. data/lib/sirena/svg/polyline.rb +35 -0
  331. data/lib/sirena/svg/rect.rb +57 -0
  332. data/lib/sirena/svg/style.rb +44 -0
  333. data/lib/sirena/svg/text.rb +72 -0
  334. data/lib/sirena/svg.rb +19 -0
  335. data/lib/sirena/text_measurement.rb +71 -0
  336. data/lib/sirena/theme/builtin/dark.yml +70 -0
  337. data/lib/sirena/theme/builtin/default.yml +80 -0
  338. data/lib/sirena/theme/builtin/high_contrast.yml +70 -0
  339. data/lib/sirena/theme/builtin/light.yml +70 -0
  340. data/lib/sirena/theme/color_palette.rb +48 -0
  341. data/lib/sirena/theme/effect_styles.rb +28 -0
  342. data/lib/sirena/theme/registry.rb +41 -0
  343. data/lib/sirena/theme/shape_styles.rb +28 -0
  344. data/lib/sirena/theme/spacing_config.rb +24 -0
  345. data/lib/sirena/theme/typography.rb +30 -0
  346. data/lib/sirena/theme.rb +69 -0
  347. data/lib/sirena/transform/architecture.rb +273 -0
  348. data/lib/sirena/transform/base.rb +199 -0
  349. data/lib/sirena/transform/block.rb +215 -0
  350. data/lib/sirena/transform/c4.rb +288 -0
  351. data/lib/sirena/transform/class_diagram.rb +296 -0
  352. data/lib/sirena/transform/er_diagram.rb +204 -0
  353. data/lib/sirena/transform/error.rb +39 -0
  354. data/lib/sirena/transform/flowchart.rb +161 -0
  355. data/lib/sirena/transform/gantt.rb +253 -0
  356. data/lib/sirena/transform/git_graph.rb +283 -0
  357. data/lib/sirena/transform/info.rb +39 -0
  358. data/lib/sirena/transform/kanban.rb +180 -0
  359. data/lib/sirena/transform/mindmap.rb +251 -0
  360. data/lib/sirena/transform/packet.rb +185 -0
  361. data/lib/sirena/transform/pie.rb +62 -0
  362. data/lib/sirena/transform/quadrant.rb +167 -0
  363. data/lib/sirena/transform/radar.rb +227 -0
  364. data/lib/sirena/transform/requirement.rb +233 -0
  365. data/lib/sirena/transform/sankey.rb +212 -0
  366. data/lib/sirena/transform/sequence.rb +143 -0
  367. data/lib/sirena/transform/state_diagram.rb +228 -0
  368. data/lib/sirena/transform/timeline.rb +139 -0
  369. data/lib/sirena/transform/treemap.rb +120 -0
  370. data/lib/sirena/transform/user_journey.rb +207 -0
  371. data/lib/sirena/transform/xy_chart.rb +273 -0
  372. data/lib/sirena/transform.rb +8 -0
  373. data/lib/sirena/version.rb +5 -0
  374. data/lib/sirena.rb +328 -0
  375. data/lib/tasks/benchmark.rake +532 -0
  376. data/lib/tasks/examples.rake +468 -0
  377. data/lib/tasks/generate_mermaid_fixtures.rake +363 -0
  378. data/lib/tasks/mermaid_fixtures.rake +46 -0
  379. data/scripts/extract_mermaid_tests.rb +493 -0
  380. data/scripts/rename_to_sirena.rb +73 -0
  381. data/sirena.gemspec +47 -0
  382. metadata +529 -0
@@ -0,0 +1,452 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../svg/document"
4
+ require_relative "../svg/circle"
5
+ require_relative "../svg/line"
6
+ require_relative "../svg/rect"
7
+ require_relative "../svg/polyline"
8
+ require_relative "../svg/text"
9
+ require_relative "../svg/group"
10
+
11
+ module Sirena
12
+ module Renderer
13
+ # Renders an XY Chart layout to SVG.
14
+ #
15
+ # The renderer converts the positioned layout structure from
16
+ # Transform::XYChart into an SVG visualization showing:
17
+ # - X and Y axes with labels
18
+ # - Grid lines
19
+ # - Data points for line charts
20
+ # - Bars for bar charts
21
+ # - Legend
22
+ #
23
+ # @example Render an XY chart
24
+ # renderer = Renderer::XYChart.new(theme: my_theme)
25
+ # svg = renderer.render(layout)
26
+ class XYChart < Base
27
+ # Default colors for datasets (cycling)
28
+ DEFAULT_COLORS = %w[
29
+ #2563eb #7c3aed #db2777 #ea580c #ca8a04
30
+ #16a34a #0891b2 #4f46e5 #c026d3 #dc2626
31
+ ].freeze
32
+
33
+ # Number of grid lines on Y-axis
34
+ GRID_LINES = 5
35
+
36
+ # Bar width ratio (fraction of available space)
37
+ BAR_WIDTH_RATIO = 0.6
38
+
39
+ # Renders the layout structure to SVG.
40
+ #
41
+ # @param layout [Hash] layout data from Transform::XYChart
42
+ # @return [Svg::Document] rendered SVG document
43
+ def render(layout)
44
+ svg = create_document_from_layout(layout)
45
+
46
+ # Store layout for rendering
47
+ @layout = layout
48
+
49
+ # Render in order: grid, axes, data, labels, legend
50
+ render_title(layout, svg) if layout[:title]
51
+ render_grid(layout, svg)
52
+ render_axes(layout, svg)
53
+ render_datasets(layout, svg)
54
+ render_axis_labels(layout, svg)
55
+ render_legend(layout, svg)
56
+
57
+ svg
58
+ end
59
+
60
+ protected
61
+
62
+ # Creates an SVG document with dimensions from layout.
63
+ #
64
+ # @param layout [Hash] layout data
65
+ # @return [Svg::Document] new SVG document
66
+ def create_document_from_layout(layout)
67
+ Svg::Document.new.tap do |doc|
68
+ doc.width = layout[:width]
69
+ doc.height = layout[:height]
70
+ doc.view_box = "0 0 #{doc.width} #{doc.height}"
71
+ end
72
+ end
73
+
74
+ # Renders the chart title.
75
+ #
76
+ # @param layout [Hash] layout data
77
+ # @param svg [Svg::Document] SVG document
78
+ # @return [void]
79
+ def render_title(layout, svg)
80
+ text = Svg::Text.new.tap do |t|
81
+ t.x = layout[:width] / 2
82
+ t.y = 30
83
+ t.text_anchor = "middle"
84
+ t.fill = theme_color(:label_text) || "#000000"
85
+ t.font_size = (theme_typography(:font_size_large) || 16).to_s
86
+ t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
87
+ t.font_weight = "bold"
88
+ t.content = layout[:title]
89
+ end
90
+
91
+ svg.add_element(text)
92
+ end
93
+
94
+ # Renders the grid lines.
95
+ #
96
+ # @param layout [Hash] layout data
97
+ # @param svg [Svg::Document] SVG document
98
+ # @return [void]
99
+ def render_grid(layout, svg)
100
+ plot_x = layout[:plot_x]
101
+ plot_y = layout[:plot_y]
102
+ plot_width = layout[:plot_width]
103
+ plot_height = layout[:plot_height]
104
+
105
+ # Horizontal grid lines (Y-axis)
106
+ GRID_LINES.times do |i|
107
+ y = plot_y + (i * plot_height / GRID_LINES)
108
+
109
+ line = Svg::Line.new.tap do |l|
110
+ l.x1 = plot_x
111
+ l.y1 = y
112
+ l.x2 = plot_x + plot_width
113
+ l.y2 = y
114
+ l.stroke = theme_color(:grid_line) || "#e5e7eb"
115
+ l.stroke_width = "1"
116
+ l.stroke_dasharray = "3,3"
117
+ end
118
+
119
+ svg.add_element(line)
120
+ end
121
+
122
+ # Vertical grid lines (X-axis) - only for categorical
123
+ if layout[:x_axis][:type] == :categorical
124
+ positions = layout[:x_axis][:positions] || []
125
+ positions.each do |pos|
126
+ x = plot_x + pos[:position]
127
+
128
+ line = Svg::Line.new.tap do |l|
129
+ l.x1 = x
130
+ l.y1 = plot_y
131
+ l.x2 = x
132
+ l.y2 = plot_y + plot_height
133
+ l.stroke = theme_color(:grid_line) || "#e5e7eb"
134
+ l.stroke_width = "1"
135
+ l.stroke_dasharray = "3,3"
136
+ end
137
+
138
+ svg.add_element(line)
139
+ end
140
+ end
141
+ end
142
+
143
+ # Renders the X and Y axes.
144
+ #
145
+ # @param layout [Hash] layout data
146
+ # @param svg [Svg::Document] SVG document
147
+ # @return [void]
148
+ def render_axes(layout, svg)
149
+ plot_x = layout[:plot_x]
150
+ plot_y = layout[:plot_y]
151
+ plot_width = layout[:plot_width]
152
+ plot_height = layout[:plot_height]
153
+
154
+ # X-axis
155
+ x_axis_line = Svg::Line.new.tap do |l|
156
+ l.x1 = plot_x
157
+ l.y1 = plot_y + plot_height
158
+ l.x2 = plot_x + plot_width
159
+ l.y2 = plot_y + plot_height
160
+ l.stroke = theme_color(:axis_line) || "#000000"
161
+ l.stroke_width = "2"
162
+ end
163
+
164
+ svg.add_element(x_axis_line)
165
+
166
+ # Y-axis
167
+ y_axis_line = Svg::Line.new.tap do |l|
168
+ l.x1 = plot_x
169
+ l.y1 = plot_y
170
+ l.x2 = plot_x
171
+ l.y2 = plot_y + plot_height
172
+ l.stroke = theme_color(:axis_line) || "#000000"
173
+ l.stroke_width = "2"
174
+ end
175
+
176
+ svg.add_element(y_axis_line)
177
+ end
178
+
179
+ # Renders axis labels and ticks.
180
+ #
181
+ # @param layout [Hash] layout data
182
+ # @param svg [Svg::Document] SVG document
183
+ # @return [void]
184
+ def render_axis_labels(layout, svg)
185
+ render_x_axis_labels(layout, svg)
186
+ render_y_axis_labels(layout, svg)
187
+ end
188
+
189
+ # Renders X-axis labels.
190
+ #
191
+ # @param layout [Hash] layout data
192
+ # @param svg [Svg::Document] SVG document
193
+ # @return [void]
194
+ def render_x_axis_labels(layout, svg)
195
+ plot_x = layout[:plot_x]
196
+ plot_y = layout[:plot_y]
197
+ plot_height = layout[:plot_height]
198
+
199
+ x_axis = layout[:x_axis]
200
+
201
+ # X-axis title
202
+ if x_axis[:label]
203
+ text = Svg::Text.new.tap do |t|
204
+ t.x = plot_x + layout[:plot_width] / 2
205
+ t.y = plot_y + plot_height + 60
206
+ t.text_anchor = "middle"
207
+ t.fill = theme_color(:label_text) || "#000000"
208
+ t.font_size = (theme_typography(:font_size_normal) || 12).to_s
209
+ t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
210
+ t.font_weight = "bold"
211
+ t.content = x_axis[:label]
212
+ end
213
+
214
+ svg.add_element(text)
215
+ end
216
+
217
+ # X-axis tick labels
218
+ if x_axis[:type] == :categorical
219
+ (x_axis[:positions] || []).each do |pos|
220
+ x = plot_x + pos[:position]
221
+ y = plot_y + plot_height + 20
222
+
223
+ text = Svg::Text.new.tap do |t|
224
+ t.x = x
225
+ t.y = y
226
+ t.text_anchor = "middle"
227
+ t.fill = theme_color(:label_text) || "#000000"
228
+ t.font_size = (theme_typography(:font_size_small) || 10).to_s
229
+ t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
230
+ t.content = pos[:label]
231
+ end
232
+
233
+ svg.add_element(text)
234
+ end
235
+ end
236
+ end
237
+
238
+ # Renders Y-axis labels.
239
+ #
240
+ # @param layout [Hash] layout data
241
+ # @param svg [Svg::Document] SVG document
242
+ # @return [void]
243
+ def render_y_axis_labels(layout, svg)
244
+ plot_x = layout[:plot_x]
245
+ plot_y = layout[:plot_y]
246
+ plot_height = layout[:plot_height]
247
+
248
+ y_axis = layout[:y_axis]
249
+
250
+ # Y-axis title
251
+ if y_axis[:label]
252
+ text = Svg::Text.new.tap do |t|
253
+ t.x = 20
254
+ t.y = plot_y + plot_height / 2
255
+ t.text_anchor = "middle"
256
+ t.fill = theme_color(:label_text) || "#000000"
257
+ t.font_size = (theme_typography(:font_size_normal) || 12).to_s
258
+ t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
259
+ t.font_weight = "bold"
260
+ t.transform = "rotate(-90, 20, #{plot_y + plot_height / 2})"
261
+ t.content = y_axis[:label]
262
+ end
263
+
264
+ svg.add_element(text)
265
+ end
266
+
267
+ # Y-axis tick labels
268
+ min = y_axis[:min]
269
+ max = y_axis[:max]
270
+
271
+ GRID_LINES.times do |i|
272
+ y = plot_y + (i * plot_height / GRID_LINES)
273
+ value = max - (i * (max - min) / GRID_LINES)
274
+
275
+ text = Svg::Text.new.tap do |t|
276
+ t.x = plot_x - 10
277
+ t.y = y + 4
278
+ t.text_anchor = "end"
279
+ t.fill = theme_color(:label_text) || "#000000"
280
+ t.font_size = (theme_typography(:font_size_small) || 10).to_s
281
+ t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
282
+ t.content = value.round(1).to_s
283
+ end
284
+
285
+ svg.add_element(text)
286
+ end
287
+ end
288
+
289
+ # Renders all datasets.
290
+ #
291
+ # @param layout [Hash] layout data
292
+ # @param svg [Svg::Document] SVG document
293
+ # @return [void]
294
+ def render_datasets(layout, svg)
295
+ layout[:datasets].each_with_index do |dataset, idx|
296
+ color = get_dataset_color(idx)
297
+
298
+ case dataset[:chart_type]
299
+ when :line
300
+ render_line_dataset(dataset, color, layout, svg)
301
+ when :bar
302
+ render_bar_dataset(dataset, color, layout, svg)
303
+ else
304
+ render_line_dataset(dataset, color, layout, svg)
305
+ end
306
+ end
307
+ end
308
+
309
+ # Gets color for a dataset based on index.
310
+ #
311
+ # @param index [Integer] dataset index
312
+ # @return [String] color value
313
+ def get_dataset_color(index)
314
+ DEFAULT_COLORS[index % DEFAULT_COLORS.length]
315
+ end
316
+
317
+ # Renders a line dataset.
318
+ #
319
+ # @param dataset [Hash] dataset data
320
+ # @param color [String] line color
321
+ # @param layout [Hash] layout data
322
+ # @param svg [Svg::Document] SVG document
323
+ # @return [void]
324
+ def render_line_dataset(dataset, color, layout, svg)
325
+ return if dataset[:points].empty?
326
+
327
+ plot_x = layout[:plot_x]
328
+ plot_y = layout[:plot_y]
329
+
330
+ # Build polyline points
331
+ points = dataset[:points].map do |point|
332
+ x = plot_x + point[:x]
333
+ y = plot_y + point[:y]
334
+ "#{x},#{y}"
335
+ end.join(" ")
336
+
337
+ polyline = Svg::Polyline.new.tap do |p|
338
+ p.points = points
339
+ p.fill = "none"
340
+ p.stroke = color
341
+ p.stroke_width = "2"
342
+ end
343
+
344
+ svg.add_element(polyline)
345
+
346
+ # Render data points as circles
347
+ dataset[:points].each do |point|
348
+ x = plot_x + point[:x]
349
+ y = plot_y + point[:y]
350
+
351
+ circle = Svg::Circle.new.tap do |c|
352
+ c.cx = x
353
+ c.cy = y
354
+ c.r = 4
355
+ c.fill = color
356
+ c.stroke = theme_color(:background) || "#ffffff"
357
+ c.stroke_width = "2"
358
+ end
359
+
360
+ svg.add_element(circle)
361
+ end
362
+ end
363
+
364
+ # Renders a bar dataset.
365
+ #
366
+ # @param dataset [Hash] dataset data
367
+ # @param color [String] bar color
368
+ # @param layout [Hash] layout data
369
+ # @param svg [Svg::Document] SVG document
370
+ # @return [void]
371
+ def render_bar_dataset(dataset, color, layout, svg)
372
+ return if dataset[:points].empty?
373
+
374
+ plot_x = layout[:plot_x]
375
+ plot_y = layout[:plot_y]
376
+ plot_height = layout[:plot_height]
377
+
378
+ # Calculate bar width
379
+ x_axis = layout[:x_axis]
380
+ bar_width = if x_axis[:type] == :categorical && x_axis[:positions]
381
+ spacing = layout[:plot_width] / x_axis[:positions].length
382
+ spacing * BAR_WIDTH_RATIO
383
+ else
384
+ 20
385
+ end
386
+
387
+ # Render each bar
388
+ dataset[:points].each do |point|
389
+ x = plot_x + point[:x] - bar_width / 2
390
+ y = plot_y + point[:y]
391
+ height = plot_height - point[:y]
392
+
393
+ rect = Svg::Rect.new.tap do |r|
394
+ r.x = x
395
+ r.y = y
396
+ r.width = bar_width
397
+ r.height = height
398
+ r.fill = color
399
+ r.fill_opacity = "0.8"
400
+ r.stroke = color
401
+ r.stroke_width = "1"
402
+ end
403
+
404
+ svg.add_element(rect)
405
+ end
406
+ end
407
+
408
+ # Renders legend for all datasets.
409
+ #
410
+ # @param layout [Hash] layout data
411
+ # @param svg [Svg::Document] SVG document
412
+ # @return [void]
413
+ def render_legend(layout, svg)
414
+ return if layout[:datasets].empty?
415
+
416
+ legend_x = layout[:width] - 150
417
+ legend_y = 60
418
+ item_height = 25
419
+
420
+ layout[:datasets].each_with_index do |dataset, idx|
421
+ color = get_dataset_color(idx)
422
+ y_offset = idx * item_height
423
+
424
+ # Color box
425
+ rect = Svg::Rect.new.tap do |r|
426
+ r.x = legend_x
427
+ r.y = legend_y + y_offset - 8
428
+ r.width = 15
429
+ r.height = 15
430
+ r.fill = color
431
+ r.stroke = "none"
432
+ end
433
+
434
+ svg.add_element(rect)
435
+
436
+ # Label
437
+ text = Svg::Text.new.tap do |t|
438
+ t.x = legend_x + 20
439
+ t.y = legend_y + y_offset + 4
440
+ t.text_anchor = "start"
441
+ t.fill = theme_color(:label_text) || "#000000"
442
+ t.font_size = (theme_typography(:font_size_small) || 10).to_s
443
+ t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
444
+ t.content = dataset[:label]
445
+ end
446
+
447
+ svg.add_element(text)
448
+ end
449
+ end
450
+ end
451
+ end
452
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'renderer/base'
4
+
5
+ module Sirena
6
+ module Renderer
7
+ end
8
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'element'
5
+
6
+ module Sirena
7
+ module Svg
8
+ # SVG Circle element <circle>
9
+ #
10
+ # Represents a circle shape with center point and radius.
11
+ class Circle < Element
12
+ attribute :cx, :float
13
+ attribute :cy, :float
14
+ attribute :r, :float
15
+
16
+ xml do
17
+ root 'circle'
18
+ map_attribute 'id', to: :id
19
+ map_attribute 'class', to: :class_name
20
+ map_attribute 'cx', to: :cx
21
+ map_attribute 'cy', to: :cy
22
+ map_attribute 'r', to: :r
23
+ map_attribute 'fill', to: :fill
24
+ map_attribute 'stroke', to: :stroke
25
+ map_attribute 'stroke-width', to: :stroke_width
26
+ map_attribute 'transform', to: :transform
27
+ map_attribute 'opacity', to: :opacity
28
+ end
29
+
30
+ protected
31
+
32
+ def element_attributes
33
+ attrs = []
34
+ attrs << %( cx="#{cx}") if cx
35
+ attrs << %( cy="#{cy}") if cy
36
+ attrs << %( r="#{r}") if r
37
+ attrs
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'element'
5
+
6
+ module Sirena
7
+ module Svg
8
+ # SVG Document root element
9
+ #
10
+ # Represents the top-level <svg> element with namespace declarations,
11
+ # viewBox, and dimension attributes. Contains all other SVG elements
12
+ # as children.
13
+ class Document < Lutaml::Model::Serializable
14
+ SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
15
+ XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'
16
+ SVG_VERSION = '1.2'
17
+
18
+ attribute :width, :float
19
+ attribute :height, :float
20
+ attribute :view_box, :string
21
+ attribute :version, :string
22
+ attribute :xmlns, :string
23
+ attribute :children, Element, collection: true
24
+
25
+ xml do
26
+ root 'svg', mixed: true
27
+ namespace SVG_NAMESPACE, 'svg'
28
+
29
+ map_attribute 'width', to: :width
30
+ map_attribute 'height', to: :height
31
+ map_attribute 'viewBox', to: :view_box
32
+ map_attribute 'version', to: :version
33
+ map_attribute 'xmlns', to: :xmlns
34
+
35
+ map_element 'g', to: :children
36
+ map_element 'rect', to: :children
37
+ map_element 'circle', to: :children
38
+ map_element 'ellipse', to: :children
39
+ map_element 'line', to: :children
40
+ map_element 'path', to: :children
41
+ map_element 'polygon', to: :children
42
+ map_element 'polyline', to: :children
43
+ map_element 'text', to: :children
44
+ end
45
+
46
+ # Initialize document with calculated viewBox if not provided
47
+ #
48
+ # @param width [Numeric] Document width
49
+ # @param height [Numeric] Document height
50
+ # @param view_box [String] ViewBox specification
51
+ def initialize(width: nil, height: nil, view_box: nil, **args)
52
+ super(**args)
53
+ self.width = width
54
+ self.height = height
55
+ self.view_box = view_box || calculate_view_box(width, height)
56
+ self.version = SVG_VERSION
57
+ self.xmlns = SVG_NAMESPACE
58
+ self.children = []
59
+ end
60
+
61
+ # Add an element to the document
62
+ #
63
+ # @param element [Element] SVG element to add
64
+ # @return [void]
65
+ def add_element(element)
66
+ children << element
67
+ end
68
+
69
+ alias << add_element
70
+
71
+ # Generate SVG XML string
72
+ #
73
+ # @return [String] XML representation of the SVG document
74
+ def to_xml
75
+ parts = ["<svg"]
76
+ parts << %( width="#{width}") if width
77
+ parts << %( height="#{height}") if height
78
+ parts << %( viewBox="#{view_box}") if view_box
79
+ parts << %( version="#{version}") if version
80
+ parts << %( xmlns="#{xmlns}") if xmlns
81
+ parts << ">"
82
+
83
+ children.each do |child|
84
+ parts << child.to_xml if child.respond_to?(:to_xml)
85
+ end
86
+
87
+ parts << "</svg>"
88
+ parts.join("\n")
89
+ end
90
+
91
+ # String representation returns XML
92
+ alias to_s to_xml
93
+
94
+ private
95
+
96
+ def calculate_view_box(width, height)
97
+ return nil unless width && height
98
+
99
+ "0 0 #{width} #{height}"
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'style'
5
+
6
+ module Sirena
7
+ module Svg
8
+ # Base class for all SVG elements.
9
+ #
10
+ # Provides common attributes and functionality shared by all SVG elements
11
+ # including styling, transformation, and identification.
12
+ class Element < Lutaml::Model::Serializable
13
+ attribute :id, :string
14
+ attribute :class_name, :string
15
+ attribute :style, Svg::Style
16
+ attribute :transform, :string
17
+ attribute :fill, :string
18
+ attribute :fill_opacity, :string
19
+ attribute :stroke, :string
20
+ attribute :stroke_width, :string
21
+ attribute :stroke_opacity, :string
22
+ attribute :opacity, :float
23
+
24
+ # Generate XML representation of this element
25
+ #
26
+ # @return [String] XML string
27
+ def to_xml
28
+ tag = self.class.name.split('::').last.downcase
29
+ attrs = build_attributes
30
+
31
+ "<#{tag}#{attrs}/>"
32
+ end
33
+
34
+ protected
35
+
36
+ # Build attribute string for XML output
37
+ #
38
+ # @return [String] formatted attribute string
39
+ def build_attributes
40
+ attrs = []
41
+ attrs << %( id="#{id}") if id
42
+ attrs << %( class="#{class_name}") if class_name
43
+ attrs << %( transform="#{transform}") if transform
44
+ attrs << %( fill="#{fill}") if fill
45
+ attrs << %( fill-opacity="#{fill_opacity}") if fill_opacity
46
+ attrs << %( stroke="#{stroke}") if stroke
47
+ attrs << %( stroke-width="#{stroke_width}") if stroke_width
48
+ attrs << %( stroke-opacity="#{stroke_opacity}") if stroke_opacity
49
+ attrs << %( opacity="#{opacity}") if opacity
50
+
51
+ # Add element-specific attributes
52
+ attrs.concat(element_attributes)
53
+
54
+ attrs.join
55
+ end
56
+
57
+ # Hook for subclasses to add their specific attributes
58
+ #
59
+ # @return [Array<String>] array of attribute strings
60
+ def element_attributes
61
+ []
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'element'
5
+
6
+ module Sirena
7
+ module Svg
8
+ # SVG Ellipse element <ellipse>
9
+ #
10
+ # Represents an ellipse shape with center point and radii.
11
+ class Ellipse < Element
12
+ attribute :cx, :float
13
+ attribute :cy, :float
14
+ attribute :rx, :float
15
+ attribute :ry, :float
16
+
17
+ xml do
18
+ root 'ellipse'
19
+ map_attribute 'id', to: :id
20
+ map_attribute 'class', to: :class_name
21
+ map_attribute 'cx', to: :cx
22
+ map_attribute 'cy', to: :cy
23
+ map_attribute 'rx', to: :rx
24
+ map_attribute 'ry', to: :ry
25
+ map_attribute 'fill', to: :fill
26
+ map_attribute 'stroke', to: :stroke
27
+ map_attribute 'stroke-width', to: :stroke_width
28
+ map_attribute 'transform', to: :transform
29
+ map_attribute 'opacity', to: :opacity
30
+ end
31
+ end
32
+ end
33
+ end