archsight 0.3.0 → 0.3.1
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 +2 -2
- data/docs/computed_annotations.md +7 -5
- data/docs/licenses.md +1 -2
- data/docs/modeling.md +5 -1
- data/docs/pages.md +63 -6
- data/docs/search.md +17 -0
- data/lib/archsight/annotations/annotation.rb +5 -4
- data/lib/archsight/annotations/computed.rb +5 -1
- data/lib/archsight/annotations/relation_resolver.rb +96 -79
- data/lib/archsight/cli.rb +3 -2
- data/lib/archsight/database.rb +2 -1
- data/lib/archsight/documentation.rb +2 -1
- data/lib/archsight/export/confluence/exporter.rb +11 -2
- data/lib/archsight/export/confluence/storage.rb +99 -8
- data/lib/archsight/export/confluence/tables.rb +78 -0
- data/lib/archsight/helpers/fenced_blocks.rb +61 -0
- data/lib/archsight/helpers/requirements_blocks.rb +90 -0
- data/lib/archsight/helpers/view_blocks.rb +108 -0
- data/lib/archsight/helpers/wiki_links.rb +39 -5
- data/lib/archsight/helpers.rb +3 -0
- data/lib/archsight/import/handlers/go_grapher.rb +4 -1
- data/lib/archsight/import/handlers/go_module_parser.rb +4 -1
- data/lib/archsight/linter.rb +25 -3
- data/lib/archsight/mcp/base.rb +38 -0
- data/lib/archsight/requirements.rb +70 -0
- data/lib/archsight/resources/analysis.rb +2 -1
- data/lib/archsight/resources/application_component.rb +5 -3
- data/lib/archsight/resources/application_interface.rb +4 -2
- data/lib/archsight/resources/application_service.rb +4 -3
- data/lib/archsight/resources/base.rb +27 -12
- data/lib/archsight/resources/business_actor.rb +5 -3
- data/lib/archsight/resources/business_product.rb +4 -3
- data/lib/archsight/resources/business_requirement.rb +4 -3
- data/lib/archsight/resources/compliance_evidence.rb +4 -2
- data/lib/archsight/resources/data_object.rb +2 -1
- data/lib/archsight/resources/import.rb +5 -2
- data/lib/archsight/resources/page.rb +4 -2
- data/lib/archsight/resources/technology_artifact.rb +4 -3
- data/lib/archsight/resources/technology_node.rb +1 -1
- data/lib/archsight/resources/technology_service.rb +4 -0
- data/lib/archsight/resources/technology_system_software.rb +4 -0
- data/lib/archsight/resources/view.rb +2 -1
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/view_table.rb +102 -0
- data/lib/archsight/web/api/json_helpers.rb +7 -5
- data/lib/archsight/web/api/openapi/spec.yaml +135 -2
- data/lib/archsight/web/api/requirements_helpers.rb +26 -0
- data/lib/archsight/web/api/routes.rb +19 -0
- data/lib/archsight/web/application.rb +6 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-D-cPRZCT.js +1 -0
- data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +1 -0
- data/lib/archsight/web/public/vue/{DocPage-uaT8CdFm.js → DocPage-DK6vNDbF.js} +1 -1
- data/lib/archsight/web/public/vue/EditorPage-BoJpQaVw.js +35 -0
- data/lib/archsight/web/public/vue/EditorPage-CbG4mc9T.css +1 -0
- data/lib/archsight/web/public/vue/ErrorPage-Ck9izEUS.css +1 -0
- data/lib/archsight/web/public/vue/ErrorPage-PGyjdtEf.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-BLiKR4zP.js +1 -0
- data/lib/archsight/web/public/vue/GraphView-BvWAbUAl.css +1 -0
- data/lib/archsight/web/public/vue/HomePage-C0lR8i2C.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-60Tt3ZNM.css +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-D3W2jJHV.js +1 -0
- data/lib/archsight/web/public/vue/KindList-BlsaRBNO.js +1 -0
- data/lib/archsight/web/public/vue/PageView-9MgHtrgl.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-3EqjpbyV.css +1 -0
- data/lib/archsight/web/public/vue/QueryError-D1FL1xgA.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +1 -0
- data/lib/archsight/web/public/vue/ResourceList-vkgFyeOY.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-Cl3O_OEr.js +1 -0
- data/lib/archsight/web/public/vue/SearchResults-DiW5XVYW.css +1 -0
- data/lib/archsight/web/public/vue/WikiPage-C-8SG67a.css +1 -0
- data/lib/archsight/web/public/vue/WikiPage-CeCQBTDS.js +13 -0
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +1 -0
- data/lib/archsight/web/public/vue/index-D7m61Ahx.js +3 -0
- data/lib/archsight/web/public/vue/index-Dbx3MXWG.css +1 -0
- data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +1 -0
- data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +3334 -0
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +1 -0
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +1 -0
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +1 -0
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +1 -0
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +1 -0
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-C71SdG-N.js → useGraphviz-DweKV7Kg.js} +1 -1
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +1 -0
- data/lib/archsight/web/public/vue.html +3 -3
- data/lib/archsight.rb +2 -0
- metadata +49 -40
- data/lib/archsight/web/public/vue/ApiDocsPage-C0y953v0.css +0 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-C_4tAWis.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-C557BJC-.js +0 -35
- data/lib/archsight/web/public/vue/EditorPage-df5N-p21.css +0 -1
- data/lib/archsight/web/public/vue/ErrorPage-Vdebmife.js +0 -2
- data/lib/archsight/web/public/vue/ErrorPage-uMDnfY5_.css +0 -1
- data/lib/archsight/web/public/vue/GraphView-BduUql2N.js +0 -1
- data/lib/archsight/web/public/vue/GraphView-Cj2V2stN.css +0 -1
- data/lib/archsight/web/public/vue/HomePage-BHUTg8Ap.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-1lSigA58.js +0 -1
- data/lib/archsight/web/public/vue/InstanceRouter-Di7f3Rya.css +0 -1
- data/lib/archsight/web/public/vue/KindList-BDZs0j6d.js +0 -1
- data/lib/archsight/web/public/vue/PageView-BYzJDwof.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-CnbhU9wm.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-xyBwu7fh.css +0 -1
- data/lib/archsight/web/public/vue/SearchResults-CNhf6VOx.js +0 -1
- data/lib/archsight/web/public/vue/SearchResults-DOHzqAy3.css +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DbmWkM7W.js +0 -13
- data/lib/archsight/web/public/vue/WikiPage-GV67QmNJ.css +0 -1
- data/lib/archsight/web/public/vue/architecture-TIHT7OUA-Bf-MWFmn.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-VYW2F7L2-CkKt8qqs.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-45OFAUF4-NwSTYPHj.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-TEB2WS4Q-DLRegrRG.js +0 -1
- data/lib/archsight/web/public/vue/index-CyVWObLU.js +0 -3
- data/lib/archsight/web/public/vue/index-DtKeHT3S.css +0 -1
- data/lib/archsight/web/public/vue/info-DKCQHKI2-EV39NzoN.js +0 -1
- data/lib/archsight/web/public/vue/mermaid-BMkGfnhm.js +0 -3279
- data/lib/archsight/web/public/vue/packet-7NZHBO7P-USljV3MZ.js +0 -1
- data/lib/archsight/web/public/vue/pie-RZYD4A2V-DChciBW2.js +0 -1
- data/lib/archsight/web/public/vue/radar-I7S5WNFK-CgGJdW1t.js +0 -1
- data/lib/archsight/web/public/vue/railroad-3IZDKUUU-Dy43vF5c.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-AHOZXSZD-BhsTH-sE.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-EBAXGLYW-BBTd_u0u.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-LSFZ7HO6-BiNTEzf0.js +0 -1
- data/lib/archsight/web/public/vue/treeView-QDETBFTQ-BfnCcf-q.js +0 -1
- data/lib/archsight/web/public/vue/treemap-6X3UGDF4-BsAORhvk.js +0 -1
- data/lib/archsight/web/public/vue/wardley-OPB4EBWU-DQvnEhRj.js +0 -1
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
|
+
require "uri"
|
|
4
5
|
require "digest"
|
|
5
6
|
require "kramdown"
|
|
6
7
|
require "kramdown-parser-gfm"
|
|
7
8
|
require_relative "../../assets"
|
|
8
9
|
require_relative "../../diagram"
|
|
9
10
|
require_relative "../../helpers/macros"
|
|
11
|
+
require_relative "../../helpers/view_blocks"
|
|
12
|
+
require_relative "../../helpers/requirements_blocks"
|
|
13
|
+
require_relative "../../query"
|
|
14
|
+
require_relative "../../requirements"
|
|
10
15
|
require_relative "diagram_links"
|
|
11
16
|
require_relative "drawio"
|
|
12
17
|
require_relative "page_url"
|
|
13
18
|
require_relative "rasterizer"
|
|
19
|
+
require_relative "tables"
|
|
14
20
|
|
|
15
21
|
module Archsight
|
|
16
22
|
module Export
|
|
@@ -31,11 +37,18 @@ module Archsight
|
|
|
31
37
|
def convert_codeblock(elem, _indent)
|
|
32
38
|
language = elem.options[:lang].to_s
|
|
33
39
|
return @options[:storage].diagram(elem.value, nil) if language == "asd"
|
|
40
|
+
return @options[:storage].view_block(elem.value) if language == "view"
|
|
41
|
+
return @options[:storage].requirements_block(elem.value) if language == "requirements"
|
|
34
42
|
|
|
35
43
|
@options[:storage].code(elem.value, language)
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
def convert_p(elem, indent)
|
|
47
|
+
only = elem.children.length == 1 && elem.children.first.type == :text ? elem.children.first.value.strip : nil
|
|
48
|
+
if only && (table = @options[:storage].table_placeholder(only))
|
|
49
|
+
return table
|
|
50
|
+
end
|
|
51
|
+
|
|
39
52
|
image = elem.children.reject { |c| c.type == :text && c.value.strip.empty? }
|
|
40
53
|
if image.length == 1 && image.first.type == :img && (block = @options[:storage].block_image(image.first.attr))
|
|
41
54
|
return "#{block}\n"
|
|
@@ -48,9 +61,15 @@ module Archsight
|
|
|
48
61
|
@options[:storage].image(elem.attr["src"].to_s, elem.attr["alt"].to_s)
|
|
49
62
|
end
|
|
50
63
|
|
|
51
|
-
# A
|
|
64
|
+
# A link to a page of Archsight becomes a link to its Confluence page, or just its text if it has none;
|
|
65
|
+
# a macro whose output is a link of its own (Jira) is shown as plain text inside a link: no link in a link
|
|
52
66
|
def convert_a(elem, indent)
|
|
53
|
-
@options[:storage]
|
|
67
|
+
storage = @options[:storage]
|
|
68
|
+
target = storage.link_target(elem.attr["href"].to_s)
|
|
69
|
+
return inner(elem, indent) unless target
|
|
70
|
+
|
|
71
|
+
elem.attr["href"] = target
|
|
72
|
+
storage.in_link { super }
|
|
54
73
|
end
|
|
55
74
|
|
|
56
75
|
def convert_text(elem, indent)
|
|
@@ -69,7 +88,8 @@ module Archsight
|
|
|
69
88
|
}.freeze
|
|
70
89
|
KNOWN_LANGUAGES = %w[actionscript3 applescript bash c# cpp css coldfusion delphi diff erlang groovy html/xml java javafx
|
|
71
90
|
javascript perl php text powershell python ruby scala sql vb yaml].freeze
|
|
72
|
-
PLACEHOLDER = /ARCHSIGHT(LINK|EMBED)(\d+)X/
|
|
91
|
+
PLACEHOLDER = /ARCHSIGHT(LINK|EMBED|TABLE)(\d+)X/
|
|
92
|
+
TABLE_PLACEHOLDER = /\AARCHSIGHTTABLE(\d+)X\z/
|
|
73
93
|
CODE = /(^[ \t]*(?:```|~~~).*?^[ \t]*(?:```|~~~)[ \t]*$|`[^`\n]*`)/m
|
|
74
94
|
|
|
75
95
|
# @param page_name [String] used to name generated attachments
|
|
@@ -101,13 +121,12 @@ module Archsight
|
|
|
101
121
|
|
|
102
122
|
# @param markdown [String] body of the page
|
|
103
123
|
# @param toc [Boolean] add a table of contents
|
|
104
|
-
# @param header [String] storage format
|
|
124
|
+
# @param header [String] storage format of the page properties, shown top left next to the banner
|
|
105
125
|
# @return [Converted]
|
|
106
126
|
def convert(markdown, toc: false, header: "")
|
|
107
127
|
document = Kramdown::Document.new(protect(markdown), input: "GFM", auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot])
|
|
108
128
|
html, = Converter.convert(document.root, document.options.merge(storage: self))
|
|
109
|
-
body
|
|
110
|
-
Converted.new(body: body, attachments: @attachments, problems: @problems)
|
|
129
|
+
Converted.new(body: layout(header, banner, (toc ? %(<ac:structured-macro ac:name="toc" />\n) : "") + html), attachments: @attachments, problems: @problems)
|
|
111
130
|
end
|
|
112
131
|
|
|
113
132
|
# ---- called by the converter
|
|
@@ -149,11 +168,43 @@ module Archsight
|
|
|
149
168
|
|
|
150
169
|
def restore(text)
|
|
151
170
|
text.gsub(PLACEHOLDER) do
|
|
171
|
+
kind = Regexp.last_match(1)
|
|
152
172
|
xml, plain = @placeholders.fetch(Regexp.last_match(2).to_i)
|
|
153
|
-
|
|
173
|
+
# a table that is not alone in its paragraph (see table_placeholder) is shown as the note
|
|
174
|
+
kind == "TABLE" || (@in_link && plain) ? plain : xml
|
|
154
175
|
end
|
|
155
176
|
end
|
|
156
177
|
|
|
178
|
+
# A ```view block as a table (the data of the moment of the export), block XML
|
|
179
|
+
def view_block(source)
|
|
180
|
+
table_xml("view block") { Tables.xml(ViewTable.from_block(database!, source)) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A ```requirements block as a table, block XML
|
|
184
|
+
def requirements_block(source)
|
|
185
|
+
table_xml("requirements block") do
|
|
186
|
+
Tables.xml(Archsight::Requirements.table(database!, Helpers::RequirementsBlocks.parse(source)), empty: "No requirements")
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The table of a paragraph that holds nothing but the placeholder of a `![[View/Name]]` (a table cannot sit
|
|
191
|
+
# inside a paragraph), nil for any other text
|
|
192
|
+
def table_placeholder(text)
|
|
193
|
+
index = text[TABLE_PLACEHOLDER, 1]
|
|
194
|
+
index && @placeholders.fetch(index.to_i).first
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Where a markdown link leads in Confluence. The links of a page point into Archsight (`/pages/<name>`,
|
|
198
|
+
# `/kinds/...`), which does not exist there: a page that has a Confluence page is linked to it, any other
|
|
199
|
+
# Archsight path is not a link.
|
|
200
|
+
# @return [String, nil] the href to use, nil for the text only
|
|
201
|
+
def link_target(href)
|
|
202
|
+
return href unless href.start_with?("/") && !href.start_with?("//")
|
|
203
|
+
|
|
204
|
+
page = archsight_page(href)
|
|
205
|
+
page && confluence_url(page)
|
|
206
|
+
end
|
|
207
|
+
|
|
157
208
|
# Runs the block for the content of a link
|
|
158
209
|
def in_link
|
|
159
210
|
previous = @in_link
|
|
@@ -165,6 +216,14 @@ module Archsight
|
|
|
165
216
|
|
|
166
217
|
private
|
|
167
218
|
|
|
219
|
+
# The page layout: the page properties top left, the hint that the page is generated top right (a narrow
|
|
220
|
+
# sidebar), then the content in a section of its own, full width
|
|
221
|
+
def layout(properties, banner, content)
|
|
222
|
+
cell = ->(xml) { %(<ac:layout-cell>#{xml.empty? ? "<p />" : xml}</ac:layout-cell>) }
|
|
223
|
+
%(<ac:layout><ac:layout-section ac:type="two_right_sidebar">#{cell.call(properties)}#{cell.call(banner)}</ac:layout-section>) +
|
|
224
|
+
%(<ac:layout-section ac:type="single">#{cell.call(content)}</ac:layout-section></ac:layout>)
|
|
225
|
+
end
|
|
226
|
+
|
|
168
227
|
def banner
|
|
169
228
|
%(<ac:structured-macro ac:name="info"><ac:rich-text-body><p>#{format(BANNER, source: h(@source))}</p></ac:rich-text-body></ac:structured-macro>\n)
|
|
170
229
|
end
|
|
@@ -180,12 +239,35 @@ module Archsight
|
|
|
180
239
|
xml = macro.confluence(value, Archsight::Helpers::Macros::Context.new(@database, nil))
|
|
181
240
|
xml && placeholder("LINK", xml, plain: (macro.plain(value) if macro.respond_to?(:plain)))
|
|
182
241
|
end
|
|
242
|
+
text = text.gsub(%r{^[ \t]*!\[\[View/([^\]|]+)\]\][ \t]*$}) { view_embed(Regexp.last_match(1).strip) || Regexp.last_match(0) }
|
|
183
243
|
text = text.gsub(/!\[\[([^\]|]+)\]\]/) { placeholder("EMBED", embed_note(Regexp.last_match(1).strip)) }
|
|
184
244
|
text.gsub(Archsight::Helpers::WikiLinks::PATTERN) do
|
|
185
245
|
placeholder("LINK", wiki_link(Regexp.last_match(1).strip, Regexp.last_match(2)&.strip))
|
|
186
246
|
end
|
|
187
247
|
end
|
|
188
248
|
|
|
249
|
+
# A View on a line of its own becomes its table; an unknown View stays an embed (a note), the linter reports it.
|
|
250
|
+
# Next to other text of its paragraph the table cannot be placed and the note is shown (`plain`).
|
|
251
|
+
def view_embed(name)
|
|
252
|
+
view = @database&.instances_by_kind("View")&.[](name)
|
|
253
|
+
return nil unless view
|
|
254
|
+
|
|
255
|
+
xml = table_xml("view #{name}") { Tables.xml(ViewTable.from_view(@database, view)) }
|
|
256
|
+
placeholder("TABLE", xml, plain: embed_note("View/#{name}"))
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# The XML of a table, or nothing and a problem (the page is not exported) if it cannot be built
|
|
260
|
+
def table_xml(what)
|
|
261
|
+
yield
|
|
262
|
+
rescue Helpers::ViewBlocks::Error, Helpers::RequirementsBlocks::Error, Archsight::Query::QueryError => e
|
|
263
|
+
problem("#{what}: #{e.message}")
|
|
264
|
+
""
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def database!
|
|
268
|
+
@database || raise(Helpers::ViewBlocks::Error, "the export has no database to read the data from")
|
|
269
|
+
end
|
|
270
|
+
|
|
189
271
|
def placeholder(kind, xml, plain: nil)
|
|
190
272
|
@placeholders << [xml, plain]
|
|
191
273
|
"ARCHSIGHT#{kind}#{@placeholders.length - 1}X"
|
|
@@ -195,9 +277,18 @@ module Archsight
|
|
|
195
277
|
"<em>#{h(reference)} (live content, shown in Archsight only)</em>"
|
|
196
278
|
end
|
|
197
279
|
|
|
280
|
+
# The page an Archsight path (`/pages/<name>`, `/kinds/Page/instances/<name>`, optionally with #anchor or ?query) shows
|
|
281
|
+
def archsight_page(href)
|
|
282
|
+
path = href.split(/[#?]/, 2).first.to_s
|
|
283
|
+
name = path[%r{\A/pages/(.+)\z}, 1] || path[%r{\A/kinds/Page/instances/([^/]+)\z}, 1]
|
|
284
|
+
name && @wiki.page_for(URI.decode_uri_component(name))
|
|
285
|
+
rescue ArgumentError
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
288
|
+
|
|
198
289
|
def wiki_link(target, label)
|
|
199
290
|
page = @wiki.page_for(target)
|
|
200
|
-
text = label ||
|
|
291
|
+
text = label || @wiki.label_for(target)
|
|
201
292
|
url = page && confluence_url(page)
|
|
202
293
|
url ? %(<a href="#{h(url)}">#{h(text)}</a>) : h(text)
|
|
203
294
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "kramdown"
|
|
5
|
+
require "kramdown-parser-gfm"
|
|
6
|
+
require_relative "../../view_table"
|
|
7
|
+
require_relative "../../helpers/macros"
|
|
8
|
+
require_relative "diagram_links"
|
|
9
|
+
|
|
10
|
+
module Archsight
|
|
11
|
+
module Export
|
|
12
|
+
module Confluence
|
|
13
|
+
# A ViewTable::Table (the result of a view or of the requirements of some resources) as a regular table in
|
|
14
|
+
# storage format. Views and requirements are live in Archsight; in Confluence they are the data of the moment
|
|
15
|
+
# of the export.
|
|
16
|
+
module Tables
|
|
17
|
+
# status of a requirement -> colour of the Confluence status macro (as the web UI colours them)
|
|
18
|
+
STATUS_COLOURS = { "implemented" => "green", "partial" => "yellow", "planned" => "blue" }.freeze
|
|
19
|
+
|
|
20
|
+
# priority of a requirement -> colour of the Confluence status macro
|
|
21
|
+
PRIORITY_COLOURS = { "must" => "red", "should" => "yellow", "may" => "grey" }.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# @param empty [String] shown instead of the table when there are no rows
|
|
26
|
+
# @return [String] block XML
|
|
27
|
+
def xml(table, empty: "No resources found")
|
|
28
|
+
out = +""
|
|
29
|
+
out << %(<p><strong>#{h(table.title)}</strong> (#{table.total} #{table.total == 1 ? "item" : "items"})</p>\n) unless table.title.to_s.empty?
|
|
30
|
+
if table.rows.empty?
|
|
31
|
+
out << %(<p><em>#{h(empty)}</em></p>\n)
|
|
32
|
+
else
|
|
33
|
+
out << "<table><tbody>\n#{row(table.columns.map { |c| "<th>#{h(c)}</th>" })}"
|
|
34
|
+
table.rows.each { |cells| out << row(cells.map { |cell| "<td>#{cell_xml(cell)}</td>" }) }
|
|
35
|
+
out << "</tbody></table>\n"
|
|
36
|
+
end
|
|
37
|
+
out << %(<p><em>#{table.cut} more #{table.cut == 1 ? "row" : "rows"} not shown, see Archsight</em></p>\n) if table.cut.positive?
|
|
38
|
+
out
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def row(cells) = "<tr>#{cells.join}</tr>\n"
|
|
42
|
+
|
|
43
|
+
def cell_xml(cell)
|
|
44
|
+
return cell.resources.map { |resource| resource_xml(resource) }.join("<br />") unless cell.resources.empty?
|
|
45
|
+
|
|
46
|
+
case cell.as
|
|
47
|
+
when :status then lozenge(cell.text, STATUS_COLOURS)
|
|
48
|
+
when :priority then cell.text.empty? ? "" : lozenge(cell.text, PRIORITY_COLOURS)
|
|
49
|
+
when :markdown then markdown_xml(cell.text)
|
|
50
|
+
else h(cell.text).gsub("\n", "<br />")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A link where the resource has a page in Confluence, else its name
|
|
55
|
+
def resource_xml(resource)
|
|
56
|
+
url = DiagramLinks.confluence_url(resource)
|
|
57
|
+
url ? %(<a href="#{h(url)}">#{h(resource.name)}</a>) : h(resource.name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def lozenge(text, colours)
|
|
61
|
+
Helpers::Macros::Status.confluence(Helpers::Macros::Status::Value.new(colours.fetch(text, "grey"), text))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Inline markdown (a requirement's story): the HTML of kramdown without the paragraph around a single line.
|
|
65
|
+
# Raw HTML in the text is shown as text (it would not be well-formed storage XML); `<https://..>` links stay.
|
|
66
|
+
def markdown_xml(text)
|
|
67
|
+
return "" if text.to_s.strip.empty?
|
|
68
|
+
|
|
69
|
+
safe = text.gsub(%r{<(?!(?:https?://|mailto:))}, "<")
|
|
70
|
+
html = Kramdown::Document.new(safe, input: "GFM", entity_output: :as_char, auto_ids: false).to_html.strip
|
|
71
|
+
html.match(%r{\A<p>((?:(?!</?p>).)*)</p>\z}m) ? Regexp.last_match(1) : html
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def h(text) = ERB::Util.html_escape(text)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "erb"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Archsight
|
|
8
|
+
module Helpers
|
|
9
|
+
# What the ```lang blocks of rendered markdown that become something else (ViewBlocks, RequirementsBlocks) have
|
|
10
|
+
# in common. Two steps, so what a block becomes stays out of the markdown post-processing (URL auto-linking,
|
|
11
|
+
# macros and `[[Name]]` links are plain-text passes over the whole HTML):
|
|
12
|
+
#
|
|
13
|
+
# html, blocks = FencedBlocks.extract(html, language: "view", prefix: "view-block") { |source, original| ... }
|
|
14
|
+
# ...further processing of html...
|
|
15
|
+
# html = FencedBlocks.restore(html, blocks, prefix: "view-block")
|
|
16
|
+
module FencedBlocks
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @yield [source, original] the block's source and its `<pre><code>` HTML; returns the HTML to put in its place
|
|
20
|
+
# @return [Array(String, Hash{String => String})] HTML with a placeholder per block, and the replacement for each
|
|
21
|
+
def extract(html, language:, prefix:)
|
|
22
|
+
blocks = {}
|
|
23
|
+
pattern = %r{<pre><code class="language-#{Regexp.escape(language)}">(.*?)</code></pre>}m
|
|
24
|
+
replaced = html.gsub(pattern) do |block|
|
|
25
|
+
placeholder = "<!--#{prefix}:#{blocks.size}-->"
|
|
26
|
+
blocks[placeholder] = yield(::CGI.unescapeHTML(Regexp.last_match(1)), block)
|
|
27
|
+
placeholder
|
|
28
|
+
end
|
|
29
|
+
[replaced, blocks]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def restore(html, blocks, prefix:)
|
|
33
|
+
return html if blocks.empty?
|
|
34
|
+
|
|
35
|
+
html.gsub(/<!--#{Regexp.escape(prefix)}:\d+-->/) { |placeholder| blocks.fetch(placeholder, placeholder) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Source of every block of that language in `markdown` (for the linter).
|
|
39
|
+
def sources(markdown, language:)
|
|
40
|
+
markdown.scan(/^[ \t]*(?:```|~~~)#{Regexp.escape(language)}[ \t]*\n(.*?)^[ \t]*(?:```|~~~)[ \t]*$/m).flatten
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `<div class="css-class"><p><strong>Label:</strong> message</p>original</div>`
|
|
44
|
+
def error_box(css_class, label, message, original)
|
|
45
|
+
%(<div class="#{css_class}"><p><strong>#{label}:</strong> #{::ERB::Util.html_escape(message)}</p>#{original}</div>)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# `<div class="css-class" data-a="..." ...>original</div>`, the values escaped
|
|
49
|
+
def placeholder(css_class, data, original)
|
|
50
|
+
attributes = data.map { |name, value| %( data-#{name}="#{::ERB::Util.html_escape(value)}") }.join
|
|
51
|
+
%(<div class="#{css_class}"#{attributes}>#{original}</div>)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A YAML mapping, without aliases or arbitrary objects
|
|
55
|
+
# @raise [Psych::Exception]
|
|
56
|
+
def load_yaml(source)
|
|
57
|
+
YAML.safe_load(source, aliases: false)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "fenced_blocks"
|
|
5
|
+
|
|
6
|
+
module Archsight
|
|
7
|
+
module Helpers
|
|
8
|
+
# Turns ```requirements fenced blocks in rendered markdown into the table of business requirements of a
|
|
9
|
+
# selection of resources (see Archsight::Requirements):
|
|
10
|
+
#
|
|
11
|
+
# ```requirements
|
|
12
|
+
# title: Requirements of the backup services # optional
|
|
13
|
+
# of: 'ApplicationService: name =~ "Backup"' # required: query selecting the resources
|
|
14
|
+
# priority: must # optional: must, should, may (one or a list)
|
|
15
|
+
# status: [implemented, partial] # optional: implemented, partial, planned
|
|
16
|
+
# ```
|
|
17
|
+
#
|
|
18
|
+
# Like views and embeds, rendering never runs the query: the block becomes a placeholder carrying the filter and
|
|
19
|
+
# the frontend asks the API (`GET /api/v1/requirements`). The source stays inside the placeholder for consumers
|
|
20
|
+
# that do not run the frontend (API, MCP). Extract and restore like DiagramBlocks, see FencedBlocks.
|
|
21
|
+
module RequirementsBlocks
|
|
22
|
+
KEYS = %w[title of priority status].freeze
|
|
23
|
+
|
|
24
|
+
class Error < StandardError; end
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def extract(html)
|
|
29
|
+
FencedBlocks.extract(html, language: "requirements", prefix: "requirements-block") { |source, original| render_block(source, original) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def restore(html, blocks)
|
|
33
|
+
FencedBlocks.restore(html, blocks, prefix: "requirements-block")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Source of every ```requirements block in `markdown` (for the linter).
|
|
37
|
+
def sources(markdown)
|
|
38
|
+
FencedBlocks.sources(markdown, language: "requirements")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The filter a block describes.
|
|
42
|
+
# @return [Hash] `{ title:, of:, priority: [String], status: [String] }`
|
|
43
|
+
# @raise [Error] if the block is not a valid filter
|
|
44
|
+
def parse(source)
|
|
45
|
+
doc = load(source)
|
|
46
|
+
raise Error, "expected a mapping with `of`, got #{doc.class.name.downcase}" unless doc.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
unknown = doc.keys.map(&:to_s) - KEYS
|
|
49
|
+
raise Error, "unknown key #{unknown.first.inspect}, known are #{KEYS.join(", ")}" unless unknown.empty?
|
|
50
|
+
|
|
51
|
+
of = doc["of"].to_s.strip
|
|
52
|
+
raise Error, "`of` is missing: the query selecting the resources" if of.empty?
|
|
53
|
+
|
|
54
|
+
check_query(of)
|
|
55
|
+
{ title: doc["title"].to_s, of: of,
|
|
56
|
+
priority: values(doc["priority"], Archsight::Requirements::PRIORITIES, "priority"),
|
|
57
|
+
status: values(doc["status"], Archsight::Requirements::STATUSES.values, "status") }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def render_block(source, original)
|
|
61
|
+
spec = parse(source)
|
|
62
|
+
data = { title: spec[:title], of: spec[:of], priority: spec[:priority].join(","), status: spec[:status].join(",") }
|
|
63
|
+
FencedBlocks.placeholder("requirements-embed", data, original)
|
|
64
|
+
rescue Error => e
|
|
65
|
+
FencedBlocks.error_box("requirements-block-error", "Requirements error", e.message, original)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def load(source)
|
|
69
|
+
FencedBlocks.load_yaml(source)
|
|
70
|
+
rescue Psych::Exception => e
|
|
71
|
+
raise Error, "invalid YAML: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def check_query(query)
|
|
75
|
+
Archsight::Query.parse(query)
|
|
76
|
+
rescue Archsight::Query::QueryError => e
|
|
77
|
+
raise Error, "invalid query: #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A scalar or a list, each value one of `allowed`
|
|
81
|
+
def values(value, allowed, name)
|
|
82
|
+
list = Array(value).map { |v| v.to_s.strip }.reject(&:empty?)
|
|
83
|
+
invalid = list - allowed
|
|
84
|
+
raise Error, "#{name} must be #{allowed.join(", ")}, not #{invalid.first.inspect}" unless invalid.empty?
|
|
85
|
+
|
|
86
|
+
list
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "fenced_blocks"
|
|
5
|
+
|
|
6
|
+
module Archsight
|
|
7
|
+
module Helpers
|
|
8
|
+
# Turns ```view fenced blocks in rendered markdown into inline views: a View defined in place, written as the
|
|
9
|
+
# View resource itself, shown like an embedded `![[View/Name]]`:
|
|
10
|
+
#
|
|
11
|
+
# ```view
|
|
12
|
+
# kind: View
|
|
13
|
+
# metadata:
|
|
14
|
+
# name: Services without backup # the title, optional
|
|
15
|
+
# annotations:
|
|
16
|
+
# view/query: 'ApplicationService: backup/mode == "none"'
|
|
17
|
+
# view/fields: name, @owner
|
|
18
|
+
# ```
|
|
19
|
+
#
|
|
20
|
+
# Like embeds, rendering never runs the query: the block becomes a placeholder carrying the spec, the frontend
|
|
21
|
+
# runs the query. The source stays inside the placeholder for consumers that do not run the frontend (API, MCP).
|
|
22
|
+
#
|
|
23
|
+
# Two steps, like DiagramBlocks, so the query text stays out of the markdown post-processing (URL auto-linking,
|
|
24
|
+
# macros and `[[Name]]` links are plain-text passes over the whole HTML):
|
|
25
|
+
#
|
|
26
|
+
# html, blocks = ViewBlocks.extract(html) # blocks -> placeholders
|
|
27
|
+
# ...further processing of html...
|
|
28
|
+
# html = ViewBlocks.restore(html, blocks) # placeholders -> view placeholders / error boxes
|
|
29
|
+
module ViewBlocks
|
|
30
|
+
TYPES = %w[list:name list:name+kind].freeze
|
|
31
|
+
KEYS = %w[view/query view/fields view/sort view/type].freeze
|
|
32
|
+
|
|
33
|
+
class Error < StandardError; end
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# @return [Array(String, Hash{String => String})] HTML with a placeholder per view block, and the rendered
|
|
38
|
+
# replacement for each placeholder
|
|
39
|
+
def extract(html)
|
|
40
|
+
FencedBlocks.extract(html, language: "view", prefix: "view-block") { |source, original| render_block(source, original) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def restore(html, blocks)
|
|
44
|
+
FencedBlocks.restore(html, blocks, prefix: "view-block")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Source of every ```view block in `markdown` (for the linter).
|
|
48
|
+
def sources(markdown)
|
|
49
|
+
FencedBlocks.sources(markdown, language: "view")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The View a block describes.
|
|
53
|
+
# @return [Hash] `{ title:, query:, fields: [String], sort: [String], type: String }`
|
|
54
|
+
# @raise [Error] if the block is not a valid View
|
|
55
|
+
def parse(source)
|
|
56
|
+
doc = load(source)
|
|
57
|
+
raise Error, "expected a View resource (a YAML mapping), got #{doc.class.name.downcase}" unless doc.is_a?(Hash)
|
|
58
|
+
raise Error, "kind must be View, not #{doc["kind"].inspect}" if doc.key?("kind") && doc["kind"] != "View"
|
|
59
|
+
|
|
60
|
+
annotations = annotations(doc)
|
|
61
|
+
query = annotations["view/query"].to_s.strip
|
|
62
|
+
raise Error, "view/query is missing" if query.empty?
|
|
63
|
+
|
|
64
|
+
check_query(query)
|
|
65
|
+
type = annotations["view/type"]&.to_s || TYPES.last
|
|
66
|
+
raise Error, "view/type must be one of #{TYPES.join(", ")}, not #{type.inspect}" unless TYPES.include?(type)
|
|
67
|
+
|
|
68
|
+
{ title: doc.dig("metadata", "name").to_s, query: query, type: type,
|
|
69
|
+
fields: list(annotations["view/fields"]), sort: list(annotations["view/sort"]) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_block(source, original)
|
|
73
|
+
spec = parse(source)
|
|
74
|
+
data = { title: spec[:title], query: spec[:query], fields: spec[:fields].join(","), sort: spec[:sort].join(","), type: spec[:type] }
|
|
75
|
+
FencedBlocks.placeholder("view-embed", data, original)
|
|
76
|
+
rescue Error => e
|
|
77
|
+
FencedBlocks.error_box("view-block-error", "View error", e.message, original)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def load(source)
|
|
81
|
+
FencedBlocks.load_yaml(source)
|
|
82
|
+
rescue Psych::Exception => e
|
|
83
|
+
raise Error, "invalid YAML: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def annotations(doc)
|
|
87
|
+
annotations = doc.dig("metadata", "annotations")
|
|
88
|
+
raise Error, "metadata.annotations must be a mapping" unless annotations.nil? || annotations.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
annotations ||= {}
|
|
91
|
+
unknown = annotations.keys.map(&:to_s).select { |key| key.start_with?("view/") } - KEYS
|
|
92
|
+
raise Error, "unknown annotation #{unknown.first.inspect}, a view knows #{KEYS.join(", ")}" unless unknown.empty?
|
|
93
|
+
|
|
94
|
+
annotations.transform_keys(&:to_s)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check_query(query)
|
|
98
|
+
Archsight::Query.parse(query)
|
|
99
|
+
rescue Archsight::Query::QueryError => e
|
|
100
|
+
raise Error, "invalid query: #{e.message}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def list(value)
|
|
104
|
+
value.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -11,6 +11,7 @@ module Archsight
|
|
|
11
11
|
# render as a `broken-link` span.
|
|
12
12
|
class WikiLinks
|
|
13
13
|
PATTERN = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/
|
|
14
|
+
HOVER_LENGTH = 200
|
|
14
15
|
|
|
15
16
|
def initialize(database, resolver: ResourceResolver.new(database))
|
|
16
17
|
@database = database
|
|
@@ -43,6 +44,26 @@ module Archsight
|
|
|
43
44
|
find_page(target)
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
# The text of a link without an explicit label: a page by its title, a resource named `Kind/Name` by its name
|
|
48
|
+
# (the kind is in the hover text), anything else as written (so a broken reference shows what must be fixed)
|
|
49
|
+
def label_for(target)
|
|
50
|
+
page = find_page(target)
|
|
51
|
+
return page.title if page
|
|
52
|
+
|
|
53
|
+
found = target.include?("/") ? @resolver.find(target) : nil
|
|
54
|
+
found.is_a?(Array) ? found.last : target
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The page or resource a target names, nil if it names none or is ambiguous
|
|
58
|
+
def target_for(target)
|
|
59
|
+
page = find_page(target)
|
|
60
|
+
return page if page
|
|
61
|
+
|
|
62
|
+
found = @resolver.find(target)
|
|
63
|
+
found = find_partial(target) || found if found == :missing && !target.include?("/")
|
|
64
|
+
found.is_a?(Array) ? @database.instances_by_kind(found.first)[found.last] : nil
|
|
65
|
+
end
|
|
66
|
+
|
|
46
67
|
# Page path for a page name, nil if there is no such page
|
|
47
68
|
def page_path(page)
|
|
48
69
|
"/pages/#{ERB::Util.url_encode(page.name)}"
|
|
@@ -55,19 +76,32 @@ module Archsight
|
|
|
55
76
|
target = ::Regexp.last_match(1).strip
|
|
56
77
|
explicit_label = ::Regexp.last_match(2)&.strip
|
|
57
78
|
href = resolve(target)
|
|
58
|
-
label = explicit_label ||
|
|
79
|
+
label = explicit_label || label_for(target)
|
|
59
80
|
text = ERB::Util.html_escape(label)
|
|
60
81
|
if href.is_a?(String)
|
|
61
|
-
%(<a href="#{ERB::Util.html_escape(href)}">#{text}</a>)
|
|
82
|
+
%(<a href="#{ERB::Util.html_escape(href)}"#{hover(target_for(target))}>#{text}</a>)
|
|
62
83
|
else
|
|
63
84
|
%(<span class="broken-link" title="#{href == :ambiguous ? "Ambiguous reference" : "Resource not found"}">#{text}</span>)
|
|
64
85
|
end
|
|
65
86
|
end
|
|
66
87
|
end
|
|
67
88
|
|
|
68
|
-
#
|
|
69
|
-
def
|
|
70
|
-
|
|
89
|
+
# `title` attribute of a link: the kind and the first line of the description (the status of a page)
|
|
90
|
+
def hover(instance)
|
|
91
|
+
return "" unless instance
|
|
92
|
+
|
|
93
|
+
kind = instance.class.name.split("::").last
|
|
94
|
+
detail = kind == "Page" ? instance.annotations["page/status"] : plain_line(instance.annotations["architecture/description"])
|
|
95
|
+
text = [kind, detail].map(&:to_s).reject(&:empty?).join("\n")
|
|
96
|
+
%( title="#{ERB::Util.html_escape(text)}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The first line of a markdown text as plain text, at most HOVER_LENGTH characters
|
|
100
|
+
def plain_line(markdown)
|
|
101
|
+
line = markdown.to_s.lines.map(&:strip).find { |l| !l.empty? }.to_s
|
|
102
|
+
line = line.gsub(%r{!?\[\[([^\]|]+/)?([^\]|]+)(?:\|([^\]]+))?\]\]}) { Regexp.last_match(3) || Regexp.last_match(2) }
|
|
103
|
+
.gsub(/!?\[([^\]]*)\]\([^)]*\)/, '\1').gsub(/\A[#>*\-\s]+/, "").delete("`*_")
|
|
104
|
+
line.length > HOVER_LENGTH ? "#{line[0, HOVER_LENGTH - 1].rstrip}…" : line
|
|
71
105
|
end
|
|
72
106
|
|
|
73
107
|
def find_page(target)
|
data/lib/archsight/helpers.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative "helpers/formatting"
|
|
4
4
|
require_relative "helpers/analysis_renderer"
|
|
5
5
|
require_relative "helpers/diagram_blocks"
|
|
6
|
+
require_relative "helpers/fenced_blocks"
|
|
7
|
+
require_relative "helpers/view_blocks"
|
|
8
|
+
require_relative "helpers/requirements_blocks"
|
|
6
9
|
require_relative "helpers/resource_resolver"
|
|
7
10
|
require_relative "helpers/embeds"
|
|
8
11
|
require_relative "helpers/wiki_links"
|
|
@@ -55,6 +55,9 @@ class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grap
|
|
|
55
55
|
output = +""
|
|
56
56
|
|
|
57
57
|
modules.each do |rel_dir, mod_name|
|
|
58
|
+
comp_name = component_name(mod_name)
|
|
59
|
+
next unless comp_name
|
|
60
|
+
|
|
58
61
|
mod_dir = rel_dir == "." ? path : File.join(path, rel_dir)
|
|
59
62
|
specs = detect_openapi_specs(mod_dir)
|
|
60
63
|
existing_interfaces = database&.instances_by_kind("ApplicationInterface") || {}
|
|
@@ -69,7 +72,7 @@ class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grap
|
|
|
69
72
|
# is expected to keep and refine, not auto-regenerated on each run.
|
|
70
73
|
component = untracked_resource_yaml(
|
|
71
74
|
kind: "ApplicationComponent",
|
|
72
|
-
name:
|
|
75
|
+
name: comp_name,
|
|
73
76
|
spec: comp_spec
|
|
74
77
|
)
|
|
75
78
|
output << YAML.dump(component)
|
|
@@ -133,9 +133,12 @@ module Archsight::Import::Handlers::GoModuleParser
|
|
|
133
133
|
# Convert a Go module path to an ApplicationComponent name.
|
|
134
134
|
# Strips the SCM host segment and joins remaining path segments with ":".
|
|
135
135
|
# "github.com/example-org/billing-service/pkg" → "example-org:billing-service:pkg"
|
|
136
|
+
# A module path without a host ("module foo") has nothing left after stripping
|
|
137
|
+
# it and gets no component, rather than one with an empty name.
|
|
138
|
+
# @return [String, nil]
|
|
136
139
|
def component_name(mod_name)
|
|
137
140
|
parts = mod_name.split("/")
|
|
138
141
|
parts.shift
|
|
139
|
-
parts.join(":")
|
|
142
|
+
parts.join(":") unless parts.empty?
|
|
140
143
|
end
|
|
141
144
|
end
|