archsight 0.3.2 → 0.4.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 +4 -4
- data/README.md +7 -1
- data/docs/archimate.md +21 -1
- data/docs/configuration.md +1 -0
- data/docs/diagram.md +15 -3
- data/docs/docker.md +4 -1
- data/docs/icons.md +32 -0
- data/docs/import.md +178 -55
- data/docs/index.md.erb +31 -8
- data/docs/modeling.md +772 -24
- data/docs/pages.md +44 -13
- data/docs/search.md +21 -1
- data/docs/togaf.md +5 -1
- data/lib/archsight/analysis/result.rb +8 -2
- data/lib/archsight/analysis/sandbox.rb +8 -0
- data/lib/archsight/analysis/typst_renderer.rb +246 -0
- data/lib/archsight/analysis.rb +1 -0
- data/lib/archsight/annotations/annotation.rb +30 -3
- data/lib/archsight/annotations/architecture_annotations.rb +47 -0
- data/lib/archsight/annotations/asset_annotations.rb +34 -0
- data/lib/archsight/annotations/link_annotations.rb +15 -0
- data/lib/archsight/annotations/resource_reference.rb +12 -0
- data/lib/archsight/annotations/risk_annotations.rb +21 -0
- data/lib/archsight/cli.rb +124 -23
- data/lib/archsight/database.rb +40 -0
- data/lib/archsight/diagram/cli.rb +50 -2
- data/lib/archsight/diagram/formatter.rb +112 -0
- data/lib/archsight/diagram/routing/edge_router/orthogonal_path.rb +38 -10
- data/lib/archsight/diagram/routing/edge_router.rb +29 -4
- data/lib/archsight/diagram/routing/edge_routing.rb +61 -5
- data/lib/archsight/diagram/style/theme.rb +7 -2
- data/lib/archsight/documentation.rb +3 -2
- data/lib/archsight/editor/page_source.rb +13 -1
- data/lib/archsight/export/confluence/analysis_output.rb +101 -0
- data/lib/archsight/export/confluence/client.rb +13 -0
- data/lib/archsight/export/confluence/diagram_links.rb +1 -1
- data/lib/archsight/export/confluence/exporter.rb +40 -13
- data/lib/archsight/export/confluence/labels.rb +25 -0
- data/lib/archsight/export/confluence/page_properties.rb +4 -6
- data/lib/archsight/export/confluence/storage.rb +38 -2
- data/lib/archsight/helpers/embeds.rb +18 -7
- data/lib/archsight/helpers/resource_resolver.rb +25 -0
- data/lib/archsight/import/executor.rb +139 -16
- data/lib/archsight/import/handler.rb +55 -6
- data/lib/archsight/import/handler_loader.rb +64 -0
- data/lib/archsight/import/handlers/cache_pruner.rb +30 -7
- data/lib/archsight/import/handlers/command.rb +120 -0
- data/lib/archsight/import/handlers/github.rb +4 -1
- data/lib/archsight/import/handlers/gitlab.rb +4 -1
- data/lib/archsight/import/handlers/go_dep_resolver.rb +10 -2
- data/lib/archsight/import/handlers/go_grapher.rb +102 -5
- data/lib/archsight/import/handlers/go_module_parser.rb +88 -0
- data/lib/archsight/import/handlers/grapher.rb +40 -1
- data/lib/archsight/import/handlers/repository.rb +3 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +3 -0
- data/lib/archsight/import/registry.rb +22 -1
- data/lib/archsight/import.rb +64 -1
- data/lib/archsight/linter/structure.rb +239 -0
- data/lib/archsight/linter.rb +217 -6
- data/lib/archsight/page_loader.rb +12 -1
- data/lib/archsight/query/evaluator.rb +13 -6
- data/lib/archsight/query/parser.rb +73 -6
- data/lib/archsight/query.rb +1 -1
- data/lib/archsight/references.rb +3 -3
- data/lib/archsight/renderer.rb +80 -23
- data/lib/archsight/resources/analysis.rb +3 -2
- data/lib/archsight/resources/application_component.rb +83 -8
- data/lib/archsight/resources/application_event.rb +79 -0
- data/lib/archsight/resources/application_service.rb +18 -2
- data/lib/archsight/resources/base.rb +58 -5
- data/lib/archsight/resources/business_actor.rb +14 -1
- data/lib/archsight/resources/business_contract.rb +72 -0
- data/lib/archsight/resources/business_control.rb +10 -2
- data/lib/archsight/resources/business_event.rb +79 -0
- data/lib/archsight/resources/business_process.rb +8 -1
- data/lib/archsight/resources/business_product.rb +12 -0
- data/lib/archsight/resources/business_role.rb +70 -0
- data/lib/archsight/resources/compliance_evidence.rb +8 -2
- data/lib/archsight/resources/data_object.rb +6 -1
- data/lib/archsight/resources/implementation_deliverable.rb +63 -0
- data/lib/archsight/resources/implementation_event.rb +52 -0
- data/lib/archsight/resources/implementation_gap.rb +49 -0
- data/lib/archsight/resources/implementation_plateau.rb +67 -0
- data/lib/archsight/resources/implementation_work_package.rb +84 -0
- data/lib/archsight/resources/import.rb +37 -11
- data/lib/archsight/resources/location.rb +56 -0
- data/lib/archsight/resources/motivation_assessment.rb +121 -0
- data/lib/archsight/resources/motivation_constraint.rb +7 -1
- data/lib/archsight/resources/motivation_driver.rb +50 -0
- data/lib/archsight/resources/motivation_goal.rb +13 -1
- data/lib/archsight/resources/motivation_principle.rb +75 -0
- data/lib/archsight/resources/motivation_requirement.rb +18 -2
- data/lib/archsight/resources/motivation_stakeholder.rb +21 -0
- data/lib/archsight/resources/page.rb +9 -7
- data/lib/archsight/resources/strategy_capability.rb +9 -0
- data/lib/archsight/resources/technology_artifact.rb +11 -4
- data/lib/archsight/resources/technology_communication_network.rb +89 -0
- data/lib/archsight/resources/technology_device.rb +88 -0
- data/lib/archsight/resources/technology_equipment.rb +75 -0
- data/lib/archsight/resources/technology_event.rb +80 -0
- data/lib/archsight/resources/technology_facility.rb +70 -0
- data/lib/archsight/resources/technology_node.rb +57 -1
- data/lib/archsight/resources/technology_service.rb +33 -0
- data/lib/archsight/resources/technology_system_software.rb +28 -0
- data/lib/archsight/resources.rb +12 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/analysis_helpers.rb +55 -0
- data/lib/archsight/web/api/json_helpers.rb +3 -37
- data/lib/archsight/web/api/openapi/spec.yaml +10 -8
- data/lib/archsight/web/api/page_helpers.rb +1 -8
- data/lib/archsight/web/api/routes.rb +7 -2
- data/lib/archsight/web/application.rb +38 -2
- data/lib/archsight/web/editor/routes.rb +2 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +1 -0
- data/lib/archsight/web/public/vue/DocPage-D52OwaKc.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-ZaCcjlIX.css +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +1 -0
- data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +1 -0
- data/lib/archsight/web/public/vue/KindList-yYcRT2BV.css +1 -0
- data/lib/archsight/web/public/vue/PageView-B93woeN3.css +1 -0
- data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +2 -0
- data/lib/archsight/web/public/vue/ResourceList-D75O87zG.css +1 -0
- data/lib/archsight/web/public/vue/{SearchResults-CyPUZZSC.css → SearchResults-BV6UHriJ.css} +1 -1
- data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +1 -0
- data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +3 -0
- data/lib/archsight/web/public/vue/WikiPage-TpmM1VAF.css +1 -0
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +1 -0
- data/lib/archsight/web/public/vue/index-BGlYCbi7.js +3 -0
- data/lib/archsight/web/public/vue/index-eUHmJ7K2.css +1 -0
- data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +1 -0
- data/lib/archsight/web/public/vue/{mermaid-BjEi5URd.js → mermaid-DjsXQ_XN.js} +35 -35
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +1 -0
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +1 -0
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +1 -0
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +1 -0
- data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +1 -0
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +1 -0
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-DweKV7Kg.js → useGraphviz-4evusK9i.js} +13 -1
- data/lib/archsight/web/public/vue/{useHighlight-D5Gf8DLl.js → useHighlight-DPguaxfm.js} +8 -8
- data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +12 -0
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +1 -0
- data/lib/archsight/web/public/vue.html +5 -5
- metadata +72 -38
- data/lib/archsight/web/public/vue/ApiDocsPage-DoOxKjG0.js +0 -1
- data/lib/archsight/web/public/vue/DocPage-CfsC3CeQ.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-CsJA0q8n.js +0 -35
- data/lib/archsight/web/public/vue/ErrorPage-OJpJz9df.js +0 -2
- data/lib/archsight/web/public/vue/GraphView-CBq6oFRV.js +0 -1
- data/lib/archsight/web/public/vue/HomePage-BwgMLgVC.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-DQz-SsQq.js +0 -1
- data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +0 -1
- data/lib/archsight/web/public/vue/KindList-4q0K9Cll.js +0 -1
- data/lib/archsight/web/public/vue/PageView-DzeGnvoS.js +0 -1
- data/lib/archsight/web/public/vue/QueryError-kg6Yf-pW.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +0 -1
- data/lib/archsight/web/public/vue/ResourceList-Df_0e0qt.js +0 -2
- data/lib/archsight/web/public/vue/SearchResults-CKgY6_wH.js +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DrvCHwJ7.js +0 -13
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +0 -1
- data/lib/archsight/web/public/vue/index-D0Q5GZRs.js +0 -3
- data/lib/archsight/web/public/vue/index-oF-iyZvk.css +0 -1
- data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +0 -1
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +0 -1
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +0 -1
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +0 -1
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +0 -1
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +0 -1
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +0 -1
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +0 -1
|
@@ -41,14 +41,15 @@ module Archsight
|
|
|
41
41
|
def candidate_paths(a, b, style, from_shape: "rectangle", to_shape: "rectangle", obstacles: [])
|
|
42
42
|
# A straight axis-aligned line joins the straight family (and the
|
|
43
43
|
# automatic choice), listed first so it wins ties against the
|
|
44
|
-
# diagonal.
|
|
44
|
+
# diagonal. It is orthogonal too (no bend at all), so an explicit
|
|
45
|
+
# `style "orthogonal"` takes it as well and only never goes diagonal.
|
|
45
46
|
aligned = StraightPath.aligned_paths(a, b, from_shape: from_shape, to_shape: to_shape)
|
|
46
47
|
straight = [StraightPath.straight_path(a, b, from_shape: from_shape, to_shape: to_shape)]
|
|
47
48
|
orthogonal = OrthogonalPath.orthogonal_candidates(a, b, obstacles)
|
|
48
49
|
|
|
49
50
|
case style
|
|
50
51
|
when "straight" then aligned + straight
|
|
51
|
-
when "orthogonal" then orthogonal
|
|
52
|
+
when "orthogonal" then aligned + orthogonal
|
|
52
53
|
else aligned + straight + orthogonal
|
|
53
54
|
end
|
|
54
55
|
end
|
|
@@ -71,8 +72,22 @@ module Archsight
|
|
|
71
72
|
# Charged relative to the best candidate in the same crossing class
|
|
72
73
|
# rather than absolutely, so however large it is it never tips the
|
|
73
74
|
# balance against `CROSSING_PENALTY`: a bridge that clears a box still
|
|
74
|
-
# beats a bend-free line through it.
|
|
75
|
+
# beats a bend-free line through it. An extra bend never costs more
|
|
76
|
+
# than `TURN_LENGTH_FACTOR` times the route's own length, though: a
|
|
77
|
+
# short zig-zag still beats a one-corner detour more than three times
|
|
78
|
+
# its length (out of a box's side and all the way along it instead of
|
|
79
|
+
# a short jog off its top).
|
|
75
80
|
TURN_PENALTY = 300.0
|
|
81
|
+
TURN_LENGTH_FACTOR = 2.0
|
|
82
|
+
|
|
83
|
+
# A bent route's run out of or into a box shorter than this hugs the
|
|
84
|
+
# box's border instead of reading as a connection to it (a turn right
|
|
85
|
+
# past the corner of the box it then enters). Every pixel short of it
|
|
86
|
+
# costs `SHORT_LEG_WEIGHT` pixels of length, so a route a little longer
|
|
87
|
+
# but with a real run in wins (see `OrthogonalPath::EXIT_SHIFT`). A
|
|
88
|
+
# straight line has no turn to hug with and is never charged.
|
|
89
|
+
MIN_END_LEG = BridgePath::BRIDGE_STUB
|
|
90
|
+
SHORT_LEG_WEIGHT = 3.0
|
|
76
91
|
|
|
77
92
|
# Two edges running right along each other for a stretch reads as
|
|
78
93
|
# clutter too, but far more mildly than either one cutting through a
|
|
@@ -130,7 +145,17 @@ module Archsight
|
|
|
130
145
|
# Folded into `length` (rather than a separate term) so the native
|
|
131
146
|
# selection kernels, which only know crossings and length, apply it too.
|
|
132
147
|
extra_turns = turns[i] - fewest_turns[crossings[i]]
|
|
133
|
-
|
|
148
|
+
turn_cost = [TURN_PENALTY, lengths[i] * TURN_LENGTH_FACTOR].min * extra_turns
|
|
149
|
+
{ path: path, crossing: crossings[i], length: lengths[i] + turn_cost + off_center_cost(path) + short_leg_cost(path) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# See `MIN_END_LEG`.
|
|
154
|
+
def short_leg_cost(path)
|
|
155
|
+
return 0.0 if path.length < 3
|
|
156
|
+
|
|
157
|
+
[path.first(2), path.last(2)].sum do |(p, q)|
|
|
158
|
+
[MIN_END_LEG - Math.hypot(q[0] - p[0], q[1] - p[1]), 0.0].max * SHORT_LEG_WEIGHT
|
|
134
159
|
end
|
|
135
160
|
end
|
|
136
161
|
|
|
@@ -37,7 +37,9 @@ module Archsight
|
|
|
37
37
|
# once it's `PORT_ORDER_PROBE` out from the side (see `order_key`);
|
|
38
38
|
# `far` -- where it finally ends up, its far endpoint -- breaks ties
|
|
39
39
|
# between lines still running together at that depth.
|
|
40
|
-
|
|
40
|
+
# `slide` is the range a straight line may move within as a whole (see
|
|
41
|
+
# `sliding_range`), nil when it can only tilt.
|
|
42
|
+
PortMember = Struct.new(:edge_path, :role, :box, :axis, :original, :approach, :order, :far, :slide, keyword_init: true)
|
|
41
43
|
|
|
42
44
|
# How many times `refine_line_overlap!` sweeps every edge. Each round
|
|
43
45
|
# re-scores every edge against every other edge's *current* path, in
|
|
@@ -183,6 +185,7 @@ module Archsight
|
|
|
183
185
|
add_port_member(groups, ep, :end, ep.to_box, ep.points.last)
|
|
184
186
|
end
|
|
185
187
|
|
|
188
|
+
groups.each_value { |members| members.each { |m| m.slide = sliding_range(m, groups) } }
|
|
186
189
|
groups.each_value { |members| apply_port_slots(members) if members.length > 1 }
|
|
187
190
|
ensure
|
|
188
191
|
@original_crossings = nil
|
|
@@ -199,6 +202,38 @@ module Archsight
|
|
|
199
202
|
end
|
|
200
203
|
end
|
|
201
204
|
|
|
205
|
+
# Where a straight line (two points, perpendicular to the side) can
|
|
206
|
+
# move as a whole, both ends together, and stay straight: within the
|
|
207
|
+
# span both boxes share on the side's axis, clear of their corners.
|
|
208
|
+
# Only when its far end is alone on its side, so moving it there
|
|
209
|
+
# never collides with that side's own port slots. Nil otherwise.
|
|
210
|
+
def sliding_range(member, groups)
|
|
211
|
+
points = member.edge_path.points
|
|
212
|
+
return nil unless points.length == 2 && EdgeRouter::PathMetrics.close?(member.approach, member.original)
|
|
213
|
+
|
|
214
|
+
far_box = member.role == :start ? member.edge_path.to_box : member.edge_path.from_box
|
|
215
|
+
far_point = member.role == :start ? points.last : points.first
|
|
216
|
+
far_side = far_box&.side_of(far_point)
|
|
217
|
+
return nil unless far_side && groups[[far_box.object_id, far_side]].length == 1
|
|
218
|
+
|
|
219
|
+
span = ->(box) { member.axis == :x ? [box.left, box.right] : [box.top, box.bottom] }
|
|
220
|
+
margin = EdgeRouter::StraightPath::ALIGNED_CORNER_MARGIN
|
|
221
|
+
low = [span.call(member.box).first, span.call(far_box).first].max + margin
|
|
222
|
+
high = [span.call(member.box).last, span.call(far_box).last].min - margin
|
|
223
|
+
low <= high ? (low..high) : nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# `member`'s route with its endpoint moved to `pos`: a straight line
|
|
227
|
+
# that may slide there moves as a whole, anything else only at this end.
|
|
228
|
+
def moved_points(member, pos)
|
|
229
|
+
delta = pos - member.original
|
|
230
|
+
points = member.edge_path.points
|
|
231
|
+
return EdgeRouter::Attachment.shift_attachment(points, member.axis, delta, at: member.role) unless member.slide&.cover?(pos)
|
|
232
|
+
|
|
233
|
+
index = member.axis == :x ? 0 : 1
|
|
234
|
+
points.map { |point| point.dup.tap { |p| p[index] += delta } }
|
|
235
|
+
end
|
|
236
|
+
|
|
202
237
|
def positioned(node)
|
|
203
238
|
Layout::PositionedNode.new(node, @boxes[node.id])
|
|
204
239
|
end
|
|
@@ -353,7 +388,7 @@ module Archsight
|
|
|
353
388
|
end
|
|
354
389
|
|
|
355
390
|
def slot_path(member, pos)
|
|
356
|
-
|
|
391
|
+
moved_points(member, pos)
|
|
357
392
|
end
|
|
358
393
|
|
|
359
394
|
def port_order(m, n)
|
|
@@ -365,8 +400,11 @@ module Archsight
|
|
|
365
400
|
# Whether moving `member`'s endpoint keeps its route's shape: it
|
|
366
401
|
# arrives perpendicular, along a run `EdgeRouter::Attachment.shift_attachment`
|
|
367
402
|
# moves as a whole -- unlike a straight two-point line, whose far end
|
|
368
|
-
# stays put, so it only ever tilts
|
|
403
|
+
# stays put, so it only ever tilts -- unless it may slide as a whole
|
|
404
|
+
# (`sliding_range`).
|
|
369
405
|
def flexible?(member)
|
|
406
|
+
return true if member.slide
|
|
407
|
+
|
|
370
408
|
member.edge_path.points.length > 2 && EdgeRouter::PathMetrics.close?(member.approach, member.original)
|
|
371
409
|
end
|
|
372
410
|
|
|
@@ -381,8 +419,12 @@ module Archsight
|
|
|
381
419
|
# the side) first tries to stay exactly where it is, anywhere along
|
|
382
420
|
# the side and not just within its band: moving only its box end
|
|
383
421
|
# would tilt it, and `place_members` already keeps it clear of every
|
|
384
|
-
# endpoint placed before it.
|
|
422
|
+
# endpoint placed before it. One that may slide as a whole
|
|
423
|
+
# (`sliding_range`) is `flexible?` instead and takes its band like the
|
|
424
|
+
# rest, so it makes room for a line that turns in beside it.
|
|
385
425
|
def band_shifts(member, band_low, band_high, side_low, side_high)
|
|
426
|
+
return shifts_for(member, slide_positions(member, band_low, band_high)) if member.slide
|
|
427
|
+
|
|
386
428
|
straight = member.approach.clamp(band_low, band_high)
|
|
387
429
|
center = (band_low + band_high) / 2.0
|
|
388
430
|
preferred = flexible?(member) ? [center, straight] : [straight, center]
|
|
@@ -392,6 +434,20 @@ module Archsight
|
|
|
392
434
|
shifts_for(member, positions)
|
|
393
435
|
end
|
|
394
436
|
|
|
437
|
+
# Where a line that may slide (`sliding_range`) tries to go, all of it
|
|
438
|
+
# inside its range so it stays straight: where it is, if that is in its
|
|
439
|
+
# band, else the band's center, then the band's ends -- clamped to the
|
|
440
|
+
# part of the band within the range. A band entirely outside the range
|
|
441
|
+
# leaves it where it is.
|
|
442
|
+
def slide_positions(member, band_low, band_high)
|
|
443
|
+
low = [band_low, member.slide.begin].max
|
|
444
|
+
high = [band_high, member.slide.end].min
|
|
445
|
+
return [member.original] if low > high
|
|
446
|
+
|
|
447
|
+
keep = member.original.between?(low, high) ? [member.original] : []
|
|
448
|
+
[*keep, ((band_low + band_high) / 2.0).clamp(low, high), low, high].uniq
|
|
449
|
+
end
|
|
450
|
+
|
|
395
451
|
# `[pos, shifted points]` for moving `member`'s endpoint to `pos`, or
|
|
396
452
|
# nil when that would make its route cross more obstacles than it
|
|
397
453
|
# already does.
|
|
@@ -405,7 +461,7 @@ module Archsight
|
|
|
405
461
|
path = member.edge_path
|
|
406
462
|
moves = positions.map do |pos|
|
|
407
463
|
delta = pos - member.original
|
|
408
|
-
[pos, delta.zero? ? path.points :
|
|
464
|
+
[pos, delta.zero? ? path.points : moved_points(member, pos), delta.zero?]
|
|
409
465
|
end
|
|
410
466
|
moved = moves.reject(&:last)
|
|
411
467
|
return moves.map { |pos, points, _| [pos, points] } if moved.empty?
|
|
@@ -4,7 +4,7 @@ module Archsight
|
|
|
4
4
|
module Diagram
|
|
5
5
|
# Every spacing/sizing/font-size value the layout and renderer use, in
|
|
6
6
|
# one place, so a diagram can pick a denser set for larger graphs:
|
|
7
|
-
# `default` > `cozy` > `compact`. Selected per diagram with a top-level
|
|
7
|
+
# `default` > `cozy` > `compact` > `tight`. Selected per diagram with a top-level
|
|
8
8
|
# `theme "cozy"` statement, or overridden from outside
|
|
9
9
|
# (`render(theme:)`, `--theme`).
|
|
10
10
|
#
|
|
@@ -113,7 +113,12 @@ module Archsight
|
|
|
113
113
|
title_inset_x: 6.0, group_title_baseline: 14.0, boundary_title_baseline: 15.0
|
|
114
114
|
)
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
# `compact` with component boxes that hug their label: ~6px above and
|
|
117
|
+
# below it instead of ~15px, 4px beside it instead of 8px, for very
|
|
118
|
+
# large graphs. Fonts, containers, gaps and actors stay as `compact`.
|
|
119
|
+
TIGHT = COMPACT.with(name: "tight", node_height: 26.0, node_label_padding: 8.0)
|
|
120
|
+
|
|
121
|
+
THEMES = [DEFAULT, COZY, COMPACT, TIGHT].to_h { |t| [t.name, t] }.freeze
|
|
117
122
|
end
|
|
118
123
|
end
|
|
119
124
|
end
|
|
@@ -6,13 +6,14 @@ require_relative "template"
|
|
|
6
6
|
module Archsight
|
|
7
7
|
# Documentation generates markdown documentation for architecture resources
|
|
8
8
|
class Documentation
|
|
9
|
-
# Layer display order (top to bottom)
|
|
10
|
-
LAYER_ORDER =
|
|
9
|
+
# Layer display order (top to bottom), without the tool's own kinds
|
|
10
|
+
LAYER_ORDER = (Archsight::Resources::LAYERS - ["other"]).freeze
|
|
11
11
|
|
|
12
12
|
# Layer display names
|
|
13
13
|
LAYER_NAMES = {
|
|
14
14
|
"motivation" => "Motivation Layer",
|
|
15
15
|
"strategy" => "Strategy Layer",
|
|
16
|
+
"implementation" => "Implementation & Migration Layer",
|
|
16
17
|
"business" => "Business Layer",
|
|
17
18
|
"application" => "Application Layer",
|
|
18
19
|
"technology" => "Technology Layer"
|
|
@@ -12,7 +12,7 @@ module Archsight
|
|
|
12
12
|
# the counterpart of Editor.to_yaml for kinds that are stored as YAML.
|
|
13
13
|
module PageSource
|
|
14
14
|
# Frontmatter keys the form knows, in the order they are written
|
|
15
|
-
KNOWN_KEYS = %w[title tags author owner status toc
|
|
15
|
+
KNOWN_KEYS = %w[title tags author owner status toc created updated properties].freeze
|
|
16
16
|
TIMESTAMPS = %w[created updated].freeze
|
|
17
17
|
ISO_TIMESTAMP = /\A\d{4}-\d{2}-\d{2}(?:T[\d:.]+(?:Z|[+-]\d\d:\d\d))?\z/
|
|
18
18
|
|
|
@@ -33,6 +33,7 @@ module Archsight
|
|
|
33
33
|
def render_fields(annotations, existing_source)
|
|
34
34
|
extras = existing_meta(existing_source)
|
|
35
35
|
extras.delete_if { |key, _| KNOWN_KEYS.include?(key) }
|
|
36
|
+
migrate_legacy_links(extras)
|
|
36
37
|
lines = []
|
|
37
38
|
lines << scalar_line("name", extras.delete("name")) if extras.key?("name")
|
|
38
39
|
KNOWN_KEYS.each do |key|
|
|
@@ -46,6 +47,17 @@ module Archsight
|
|
|
46
47
|
"---\n#{lines.join}---\n#{body(annotations["page/content"])}"
|
|
47
48
|
end
|
|
48
49
|
|
|
50
|
+
# The old `confluence:` key becomes `links: { confluence: ... }` when the page is saved; the links mapping is
|
|
51
|
+
# not a form field, so it is kept as it is
|
|
52
|
+
def migrate_legacy_links(extras)
|
|
53
|
+
legacy = extras.delete("confluence")
|
|
54
|
+
return if legacy.nil?
|
|
55
|
+
|
|
56
|
+
links = extras["links"].is_a?(Hash) ? extras["links"] : {}
|
|
57
|
+
links["confluence"] ||= legacy
|
|
58
|
+
extras["links"] = links
|
|
59
|
+
end
|
|
60
|
+
|
|
49
61
|
# Check that a submitted file still is the page it replaces
|
|
50
62
|
# @raise [FileWriter::WriteError]
|
|
51
63
|
def validate!(source, path:, name:)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "kramdown"
|
|
5
|
+
require "kramdown-parser-gfm"
|
|
6
|
+
|
|
7
|
+
module Archsight
|
|
8
|
+
module Export
|
|
9
|
+
module Confluence
|
|
10
|
+
# The result of an analysis (`![[Analysis/Name]]` in a page, run at the export) in storage format: headings,
|
|
11
|
+
# text, tables and lists as Confluence content, messages as info/note/warning panels, code as a code macro.
|
|
12
|
+
# Like a view, it is the data of the moment of the export. The XML holds nothing that changes between two
|
|
13
|
+
# runs on the same data (no time, no duration), so an unchanged result leaves the page unchanged.
|
|
14
|
+
class AnalysisOutput
|
|
15
|
+
# level of a message -> Confluence panel macro
|
|
16
|
+
PANELS = { error: "warning", warning: "note", info: "info" }.freeze
|
|
17
|
+
LINK = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/
|
|
18
|
+
BREAK = %r{<br\s*/?>|\r?\n}i
|
|
19
|
+
|
|
20
|
+
# @param link [Proc] (target, label) -> inline XML of a `[[...]]` link
|
|
21
|
+
# @param code [Proc] (source, language) -> block XML of a code macro
|
|
22
|
+
def initialize(link:, code:)
|
|
23
|
+
@link = link
|
|
24
|
+
@code = code
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [String] block XML
|
|
28
|
+
def xml(name, result)
|
|
29
|
+
out = %(<p><strong>#{h(name)}</strong> (result of the analysis at the export)</p>\n)
|
|
30
|
+
return out << %(<p><em>The analysis failed: #{h(result.error)}</em></p>\n) unless result.success?
|
|
31
|
+
|
|
32
|
+
result.sections.each { |section| out << section_xml(section).to_s }
|
|
33
|
+
out
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def section_xml(section)
|
|
39
|
+
case section[:type]
|
|
40
|
+
when :heading then heading(section)
|
|
41
|
+
when :text then block_markdown(section[:content])
|
|
42
|
+
when :table then table(section)
|
|
43
|
+
when :list then "<ul>#{section[:items].map { |item| "<li>#{inline(item)}</li>" }.join}</ul>\n"
|
|
44
|
+
when :message then panel(section)
|
|
45
|
+
when :code then @code.call(section[:content].to_s, section[:lang].to_s)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# level 0 is a section of the analysis: below the page's own headings
|
|
50
|
+
def heading(section)
|
|
51
|
+
level = (section[:level].to_i + 3).clamp(3, 6)
|
|
52
|
+
"<h#{level}>#{h(section[:text])}</h#{level}>\n"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def table(section)
|
|
56
|
+
head = "<tr>#{section[:headers].map { |header| "<th>#{h(header)}</th>" }.join}</tr>\n"
|
|
57
|
+
rows = section[:rows].map { |cells| "<tr>#{cells.map { |cell| "<td>#{inline(cell)}</td>" }.join}</tr>\n" }
|
|
58
|
+
"<table><tbody>\n#{head}#{rows.join}</tbody></table>\n"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def panel(section)
|
|
62
|
+
macro = PANELS.fetch(section[:level], "info")
|
|
63
|
+
%(<ac:structured-macro ac:name="#{macro}"><ac:rich-text-body><p>#{inline(section[:message])}</p></ac:rich-text-body></ac:structured-macro>\n)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A cell, list item or message: markdown and [[links]] inline, a `<br>` or newline breaks the line, other
|
|
67
|
+
# HTML is text
|
|
68
|
+
def inline(value)
|
|
69
|
+
value.to_s.split(BREAK).map { |line| inline_markdown(line) }.join("<br />")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def inline_markdown(text)
|
|
73
|
+
with_links(text) do |marked|
|
|
74
|
+
html = markdown(marked)
|
|
75
|
+
html.match(%r{\A<p>((?:(?!</?p>).)*)</p>\z}m) ? Regexp.last_match(1) : html
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def block_markdown(text) = "#{with_links(text.to_s) { |marked| markdown(marked) }}\n"
|
|
80
|
+
|
|
81
|
+
# Keeps the [[links]] out of kramdown (it would split the brackets) and puts their XML into its result
|
|
82
|
+
def with_links(text)
|
|
83
|
+
links = []
|
|
84
|
+
marked = text.gsub(LINK) { (links << [Regexp.last_match(1).strip, Regexp.last_match(2)&.strip]) && "ARCHSIGHTAL#{links.size - 1}X" }
|
|
85
|
+
yield(marked).gsub(/ARCHSIGHTAL(\d+)X/) { @link.call(*links[Regexp.last_match(1).to_i]) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# kramdown HTML of the text: raw HTML is shown as text (it would not be well-formed storage XML)
|
|
89
|
+
def markdown(text)
|
|
90
|
+
return "" if text.strip.empty?
|
|
91
|
+
|
|
92
|
+
safe = text.gsub(%r{<(?!(?:https?://|mailto:))}, "<")
|
|
93
|
+
Kramdown::Document.new(safe, input: "GFM", entity_output: :as_char, auto_ids: false, hard_wrap: false).to_html.strip
|
|
94
|
+
.gsub(%r{<(br|hr|img)([^>]*?)\s*/?>}, '<\1\2 />')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def h(text) = ERB::Util.html_escape(text)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -97,6 +97,19 @@ module Archsight
|
|
|
97
97
|
call("DELETE", "/rest/api/content/#{attachment_id}")
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# The names of the labels of a content
|
|
101
|
+
def labels(id)
|
|
102
|
+
(get("/rest/api/content/#{id}/label", limit: 500)["results"] || []).map { |label| label["name"] }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_labels(id, names)
|
|
106
|
+
post("/rest/api/content/#{id}/label", names.map { |name| { prefix: "global", name: name } })
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def remove_label(id, name)
|
|
110
|
+
call("DELETE", "/rest/api/content/#{id}/label", query: { name: name })
|
|
111
|
+
end
|
|
112
|
+
|
|
100
113
|
# Let only this user edit the page (best effort: the caller reports a failure).
|
|
101
114
|
def restrict_update_to(id, username)
|
|
102
115
|
put("/rest/api/content/#{id}/restriction", [{ operation: "update", restrictions: { user: [{ type: "known", username: username }] } }])
|
|
@@ -26,7 +26,7 @@ module Archsight
|
|
|
26
26
|
|
|
27
27
|
# @return [String, nil] where the page lives in Confluence, nil if it has no Confluence page
|
|
28
28
|
def self.confluence_url(page)
|
|
29
|
-
link = page&.annotations&.fetch("
|
|
29
|
+
link = page&.annotations&.fetch("link/confluence", nil).to_s.strip
|
|
30
30
|
return nil if link.empty?
|
|
31
31
|
|
|
32
32
|
parsed = PageUrl.parse(link)
|
|
@@ -12,6 +12,7 @@ require_relative "credentials"
|
|
|
12
12
|
require_relative "page_url"
|
|
13
13
|
require_relative "storage"
|
|
14
14
|
require_relative "page_properties"
|
|
15
|
+
require_relative "labels"
|
|
15
16
|
|
|
16
17
|
module Archsight
|
|
17
18
|
module Export
|
|
@@ -21,7 +22,8 @@ module Archsight
|
|
|
21
22
|
# A page is only written when Confluence still holds what Archsight wrote last time: the exporter leaves a
|
|
22
23
|
# marker (a content property with the page version it created) and refuses to overwrite a page that has
|
|
23
24
|
# no marker or a newer version, unless `force` is set. Every written version says in its message that it
|
|
24
|
-
# was generated, and the page is locked for other editors where the server allows it.
|
|
25
|
+
# was generated, and the page is locked for other editors where the server allows it. The page's tags
|
|
26
|
+
# become its labels; the exporter only removes labels it added itself (recorded in the marker).
|
|
25
27
|
class Exporter
|
|
26
28
|
PROPERTY = "archsight"
|
|
27
29
|
|
|
@@ -76,7 +78,7 @@ module Archsight
|
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def link(page)
|
|
79
|
-
value = page.annotations["
|
|
81
|
+
value = page.annotations["link/confluence"].to_s.strip
|
|
80
82
|
value.empty? ? nil : value
|
|
81
83
|
end
|
|
82
84
|
|
|
@@ -138,23 +140,48 @@ module Archsight
|
|
|
138
140
|
current = content.dig("version", "number")
|
|
139
141
|
body_hash = Digest::SHA256.hexdigest(converted.body)
|
|
140
142
|
files = converted.attachments.transform_values(&:sha)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
labels = Labels.for(page)
|
|
144
|
+
body_changed = blocked || marker["bodyHash"] != body_hash || marker["attachments"] != files
|
|
145
|
+
labels_changed = (marker&.fetch("labels", nil) || []) != labels
|
|
146
|
+
return Result.new(page: page.name, status: :unchanged, message: "version #{current}") unless body_changed || labels_changed
|
|
147
|
+
return would_export(page, current, blocked, body_changed, labels) if @dry_run
|
|
148
|
+
|
|
149
|
+
version = body_changed ? write_body(page, client, content, converted, marker, blocked, id) : current
|
|
150
|
+
own = marker&.fetch("ownLabels", nil) || []
|
|
151
|
+
own = sync_labels(client, id, labels, own) if labels_changed
|
|
152
|
+
state = { "version" => version, "bodyHash" => body_hash, "attachments" => files, "labels" => labels,
|
|
153
|
+
"ownLabels" => own, "page" => page.name, "exportedAt" => Time.now.utc.iso8601,
|
|
154
|
+
"archsight" => Archsight::VERSION }
|
|
155
|
+
client.save_property(id, PROPERTY, state, property)
|
|
156
|
+
return Result.new(page: page.name, status: :exported, message: "labels updated (version #{version})") unless body_changed
|
|
157
|
+
|
|
158
|
+
remove_stale_attachments(client, id, marker, files)
|
|
159
|
+
Result.new(page: page.name, status: :exported, message: "version #{version}#{lock(client, id)}")
|
|
160
|
+
end
|
|
143
161
|
|
|
162
|
+
# Uploads the attachments and writes the new version; returns its number
|
|
163
|
+
def write_body(page, client, content, converted, marker, blocked, id)
|
|
164
|
+
current = content.dig("version", "number")
|
|
144
165
|
upload(client, id, converted.attachments, marker&.fetch("attachments", nil) || {})
|
|
145
166
|
updated = client.update_page(id, title: content["title"], space: content.dig("space", "key"), body: converted.body,
|
|
146
167
|
version: current + 1, message: message(page, blocked, marker, current))
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
168
|
+
updated.dig("version", "number")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Gives the page the labels of its tags and removes the labels an earlier export added that are no tag any
|
|
172
|
+
# more; labels somebody else set are left alone. Returns the labels the exporter owns now.
|
|
173
|
+
def sync_labels(client, id, labels, own)
|
|
174
|
+
present = client.labels(id)
|
|
175
|
+
added = labels - present
|
|
176
|
+
client.add_labels(id, added) if added.any?
|
|
177
|
+
(own - labels).each { |name| client.remove_label(id, name) if present.include?(name) }
|
|
178
|
+
((own & labels) + added).uniq.sort
|
|
153
179
|
end
|
|
154
180
|
|
|
155
|
-
def would_export(page, current, blocked)
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
def would_export(page, current, blocked, body_changed, labels)
|
|
182
|
+
message = body_changed ? "would write version #{current + 1}#{" (overwriting: #{blocked})" if blocked}" : "would update the labels"
|
|
183
|
+
message += " with the labels #{labels.join(", ")}" if body_changed && labels.any?
|
|
184
|
+
Result.new(page: page.name, status: :would_export, message: message)
|
|
158
185
|
end
|
|
159
186
|
|
|
160
187
|
def upload(client, id, attachments, known)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
module Export
|
|
5
|
+
module Confluence
|
|
6
|
+
# The `page/tags` of a page as Confluence labels. A label is lowercase and cannot contain spaces or any of
|
|
7
|
+
# `!#&()*,.:;<>?@[]^`, so those become a dash: `Team:BSS` is the label `team-bss`.
|
|
8
|
+
module Labels
|
|
9
|
+
FORBIDDEN = /[\s!#&()*,.:;<>?@\[\]^]+/
|
|
10
|
+
MAX_LENGTH = 255
|
|
11
|
+
|
|
12
|
+
# @return [Array<String>] the labels of the page, sorted, without duplicates
|
|
13
|
+
def self.for(page)
|
|
14
|
+
page.annotations["page/tags"].to_s.split(",").filter_map { |tag| label(tag) }.uniq.sort
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [String, nil] the label for a tag, nil if nothing of it is left
|
|
18
|
+
def self.label(tag)
|
|
19
|
+
name = tag.to_s.downcase.gsub(FORBIDDEN, "-").squeeze("-").delete_prefix("-").delete_suffix("-")[0, MAX_LENGTH]
|
|
20
|
+
name.empty? ? nil : name
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -9,9 +9,9 @@ module Archsight
|
|
|
9
9
|
module Export
|
|
10
10
|
module Confluence
|
|
11
11
|
# The frontmatter of a page as the Page Properties (`details`) macro of Confluence: a two column table
|
|
12
|
-
# with the status, owner, author, teams
|
|
12
|
+
# with the status, owner, author, teams and every further property. It is the counterpart of what the
|
|
13
13
|
# import lifts out of such a table, and keeps Confluence's property reports working. People are written as
|
|
14
|
-
# text (a name), not as user links.
|
|
14
|
+
# text (a name), not as user links. The other tags are no property: they are the page's labels (`Labels`).
|
|
15
15
|
module PageProperties
|
|
16
16
|
STATUS_COLOURS = {
|
|
17
17
|
"wip" => "yellow", "draft" => "yellow", "rfc" => "blue", "review" => "blue", "proposed" => "blue",
|
|
@@ -36,13 +36,11 @@ module Archsight
|
|
|
36
36
|
# @return [Array<Array(String, String)>] label and the storage XML of the value
|
|
37
37
|
def rows(page)
|
|
38
38
|
annotations = page.annotations
|
|
39
|
-
|
|
40
|
-
teams, other = tags.partition { |tag| tag.start_with?(TEAM_PREFIX) }
|
|
39
|
+
teams = annotations["page/tags"].to_s.split(",").map(&:strip).select { |tag| tag.start_with?(TEAM_PREFIX) }
|
|
41
40
|
rows = [["Document status", status(annotations["page/status"])],
|
|
42
41
|
["Document owner", text(person(annotations["page/owner"]))],
|
|
43
42
|
["Document author", text(person(annotations["page/author"]))],
|
|
44
|
-
["Teams", text(teams.map { |t| t.delete_prefix(TEAM_PREFIX).strip }.join(", "))]
|
|
45
|
-
["Tags", text(other.join(", "))]]
|
|
43
|
+
["Teams", text(teams.map { |t| t.delete_prefix(TEAM_PREFIX).strip }.join(", "))]]
|
|
46
44
|
rows += page.properties.map { |key, value| [key, text(value)] }
|
|
47
45
|
rows.reject { |_, value| value.empty? }
|
|
48
46
|
end
|
|
@@ -17,6 +17,7 @@ require_relative "drawio"
|
|
|
17
17
|
require_relative "page_url"
|
|
18
18
|
require_relative "rasterizer"
|
|
19
19
|
require_relative "tables"
|
|
20
|
+
require_relative "analysis_output"
|
|
20
21
|
|
|
21
22
|
module Archsight
|
|
22
23
|
module Export
|
|
@@ -229,9 +230,31 @@ module Archsight
|
|
|
229
230
|
end
|
|
230
231
|
|
|
231
232
|
# {macros}, [[Target|label]] and ![[View/Name]] become placeholders before kramdown sees them (it would split the
|
|
232
|
-
# brackets), except in code.
|
|
233
|
+
# brackets), except in code; embedded descriptions are written in first.
|
|
233
234
|
def protect(markdown)
|
|
234
|
-
markdown
|
|
235
|
+
outside_code(embed_descriptions(markdown)) { |text| protect_text(text) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def outside_code(markdown, &)
|
|
239
|
+
markdown.split(CODE).each_with_index.map { |part, i| i.odd? ? part : yield(part) }.join
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# The description of a resource embedded on a line of its own (`![[MotivationGoal/Name]]`) is written into the
|
|
243
|
+
# page: it is text, not live content. Embeds in that description stay notes (they are not expanded again).
|
|
244
|
+
def embed_descriptions(markdown)
|
|
245
|
+
outside_code(markdown) do |text|
|
|
246
|
+
text.gsub(/^[ \t]*!\[\[([^\]|]+)\]\][ \t]*$/) { embedded_description(Regexp.last_match(1).strip) || Regexp.last_match(0) }
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def embedded_description(reference)
|
|
251
|
+
kind, name = reference.split("/", 2)
|
|
252
|
+
return if name.nil? || Archsight::Helpers::Embeds::LIVE_KINDS.include?(kind) ||
|
|
253
|
+
!Archsight::Helpers::Embeds.embeddable_kind?(kind)
|
|
254
|
+
|
|
255
|
+
instance = @database&.instances_by_kind(kind)&.[](name)
|
|
256
|
+
description = instance&.annotations.to_h["architecture/description"].to_s.strip
|
|
257
|
+
description.empty? ? nil : description
|
|
235
258
|
end
|
|
236
259
|
|
|
237
260
|
def protect_text(text)
|
|
@@ -240,6 +263,7 @@ module Archsight
|
|
|
240
263
|
xml && placeholder("LINK", xml, plain: (macro.plain(value) if macro.respond_to?(:plain)))
|
|
241
264
|
end
|
|
242
265
|
text = text.gsub(%r{^[ \t]*!\[\[View/([^\]|]+)\]\][ \t]*$}) { view_embed(Regexp.last_match(1).strip) || Regexp.last_match(0) }
|
|
266
|
+
text = text.gsub(%r{^[ \t]*!\[\[Analysis/([^\]|]+)\]\][ \t]*$}) { analysis_embed(Regexp.last_match(1).strip) || Regexp.last_match(0) }
|
|
243
267
|
text = text.gsub(/!\[\[([^\]|]+)\]\]/) { placeholder("EMBED", embed_note(Regexp.last_match(1).strip)) }
|
|
244
268
|
text.gsub(Archsight::Helpers::WikiLinks::PATTERN) do
|
|
245
269
|
placeholder("LINK", wiki_link(Regexp.last_match(1).strip, Regexp.last_match(2)&.strip))
|
|
@@ -256,6 +280,18 @@ module Archsight
|
|
|
256
280
|
placeholder("TABLE", xml, plain: embed_note("View/#{name}"))
|
|
257
281
|
end
|
|
258
282
|
|
|
283
|
+
# An Analysis on a line of its own is run and its result written into the page (the data of the moment, like a
|
|
284
|
+
# view); an unknown Analysis stays an embed (a note), the linter reports it.
|
|
285
|
+
def analysis_embed(name)
|
|
286
|
+
analysis = @database&.instances_by_kind("Analysis")&.[](name)
|
|
287
|
+
return nil unless analysis
|
|
288
|
+
|
|
289
|
+
require_relative "../../analysis"
|
|
290
|
+
result = Archsight::Analysis::Executor.new(@database).execute(analysis)
|
|
291
|
+
output = AnalysisOutput.new(link: ->(target, label) { wiki_link(target, label) }, code: ->(source, language) { code(source, language) })
|
|
292
|
+
placeholder("TABLE", output.xml(name, result), plain: embed_note("Analysis/#{name}"))
|
|
293
|
+
end
|
|
294
|
+
|
|
259
295
|
# The XML of a table, or nothing and a problem (the page is not exported) if it cannot be built
|
|
260
296
|
def table_xml(what)
|
|
261
297
|
yield
|