archsight 0.4.0 → 0.4.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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/docs/configuration.md +1 -1
  3. data/docs/modeling.md +8 -0
  4. data/docs/pages.md +2 -2
  5. data/lib/archsight/analysis/typst_renderer.rb +2 -3
  6. data/lib/archsight/export/confluence/analysis_output.rb +3 -4
  7. data/lib/archsight/export/confluence/exporter.rb +3 -1
  8. data/lib/archsight/export/confluence/page_properties.rb +2 -3
  9. data/lib/archsight/export/confluence/storage.rb +25 -12
  10. data/lib/archsight/export/confluence/tables.rb +2 -3
  11. data/lib/archsight/git_source.rb +81 -0
  12. data/lib/archsight/helpers/asset_images.rb +3 -4
  13. data/lib/archsight/import/executor.rb +1 -0
  14. data/lib/archsight/import.rb +27 -2
  15. data/lib/archsight/linter.rb +2 -2
  16. data/lib/archsight/markdown.rb +48 -0
  17. data/lib/archsight/openapi.rb +169 -0
  18. data/lib/archsight/page_tree.rb +2 -3
  19. data/lib/archsight/resources/application_interface.rb +12 -0
  20. data/lib/archsight/resources/technology_communication_network.rb +2 -0
  21. data/lib/archsight/version.rb +1 -1
  22. data/lib/archsight/web/api/docs.rb +2 -1
  23. data/lib/archsight/web/api/openapi/spec.yaml +136 -27
  24. data/lib/archsight/web/api/routes.rb +2 -2
  25. data/lib/archsight/web/application.rb +2 -2
  26. data/lib/archsight/web/public/vendor/redoc/LICENSE +22 -0
  27. data/lib/archsight/web/public/vendor/redoc/VERSION +1 -0
  28. data/lib/archsight/web/public/vendor/redoc/api.html +27 -0
  29. data/lib/archsight/web/public/vendor/redoc/api.js +19 -0
  30. data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js +1851 -0
  31. data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js.LICENSE.txt +102 -0
  32. data/lib/archsight/web/public/vue/{DocPage-D52OwaKc.js → DocPage-C2ubTXH7.js} +1 -1
  33. data/lib/archsight/web/public/vue/EditorPage-Dso6PLMn.js +35 -0
  34. data/lib/archsight/web/public/vue/ErrorPage-DnZ6YJLi.js +2 -0
  35. data/lib/archsight/web/public/vue/GraphView-CTl03Yre.js +1 -0
  36. data/lib/archsight/web/public/vue/HomePage-C339nihQ.js +2 -0
  37. data/lib/archsight/web/public/vue/InstanceRouter-DFW7tOrD.js +1 -0
  38. data/lib/archsight/web/public/vue/KindList-C0oaM3CF.js +1 -0
  39. data/lib/archsight/web/public/vue/PageView-OZB6G9Wu.js +1 -0
  40. data/lib/archsight/web/public/vue/QueryError-C2xLkCEm.js +1 -0
  41. data/lib/archsight/web/public/vue/ResourceList-DooWFwQP.js +2 -0
  42. data/lib/archsight/web/public/vue/SearchResults-BK-HjFnv.js +1 -0
  43. data/lib/archsight/web/public/vue/WikiPage-B2FYmeHL.js +3 -0
  44. data/lib/archsight/web/public/vue/architecture-WOLXFQ4H-BGN-TV5r.js +1 -0
  45. data/lib/archsight/web/public/vue/cynefin-EF2NZ3EQ-CZd6g_tq.js +1 -0
  46. data/lib/archsight/web/public/vue/eventmodeling-K75KTNOO-CYnbEx8O.js +1 -0
  47. data/lib/archsight/web/public/vue/gitGraph-VSP46ZUC-DialfQSk.js +1 -0
  48. data/lib/archsight/web/public/vue/graphviz-1lWBhpEX.js +3 -0
  49. data/lib/archsight/web/public/vue/{index-eUHmJ7K2.css → index-BLSE5DSv.css} +1 -1
  50. data/lib/archsight/web/public/vue/index-CLuLTE5I.js +3 -0
  51. data/lib/archsight/web/public/vue/info-OHQRW6UA-BuhOW7ss.js +1 -0
  52. data/lib/archsight/web/public/vue/katex-B_JcIwqj.js +257 -0
  53. data/lib/archsight/web/public/vue/mermaid-C3bOamfn.js +3999 -0
  54. data/lib/archsight/web/public/vue/packet-JDAUHWVQ-CUeRz7w4.js +1 -0
  55. data/lib/archsight/web/public/vue/pie-XZMESJXO-DnuewJI6.js +1 -0
  56. data/lib/archsight/web/public/vue/radar-ABXABTNO-Bi9l8bcv.js +1 -0
  57. data/lib/archsight/web/public/vue/railroad-I3PHUGI6-DSlxFbxk.js +1 -0
  58. data/lib/archsight/web/public/vue/railroad-abnf-LNEFI6M7-D6Kqu8wu.js +1 -0
  59. data/lib/archsight/web/public/vue/railroad-ebnf-DWYD2UWJ-MR7uCYT0.js +1 -0
  60. data/lib/archsight/web/public/vue/railroad-peg-JR7BVNR7-BL7aKIc-.js +1 -0
  61. data/lib/archsight/web/public/vue/rolldown-runtime-COiEfhsA.js +1 -0
  62. data/lib/archsight/web/public/vue/treeView-D4JQ5SDB-CamtB04D.js +1 -0
  63. data/lib/archsight/web/public/vue/treemap-SAJKECNS-DeQxu_HM.js +1 -0
  64. data/lib/archsight/web/public/vue/{useGraphviz-4evusK9i.js → useGraphviz-Dnm2pkon.js} +2 -2
  65. data/lib/archsight/web/public/vue/useHighlight-BH32C7yt.js +10 -0
  66. data/lib/archsight/web/public/vue/usePanZoom-HxgEsqq2.js +12 -0
  67. data/lib/archsight/web/public/vue/wardley-7MLQ67FV-BvKIB6Tv.js +1 -0
  68. data/lib/archsight/web/public/vue.html +4 -4
  69. metadata +46 -39
  70. data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +0 -1
  71. data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +0 -1
  72. data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +0 -35
  73. data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +0 -2
  74. data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +0 -1
  75. data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +0 -2
  76. data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +0 -1
  77. data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +0 -1
  78. data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +0 -1
  79. data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +0 -1
  80. data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +0 -2
  81. data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +0 -1
  82. data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +0 -3
  83. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +0 -1
  84. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +0 -1
  85. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +0 -1
  86. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +0 -1
  87. data/lib/archsight/web/public/vue/graphviz-SXWTgZH5.js +0 -3
  88. data/lib/archsight/web/public/vue/index-BGlYCbi7.js +0 -3
  89. data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +0 -1
  90. data/lib/archsight/web/public/vue/katex-CXMH3UgJ.js +0 -257
  91. data/lib/archsight/web/public/vue/mermaid-DjsXQ_XN.js +0 -3334
  92. data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +0 -1
  93. data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +0 -1
  94. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +0 -1
  95. data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +0 -1
  96. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +0 -1
  97. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +0 -1
  98. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +0 -1
  99. data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +0 -1
  100. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +0 -1
  101. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +0 -1
  102. data/lib/archsight/web/public/vue/useHighlight-DPguaxfm.js +0 -10
  103. data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +0 -12
  104. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00045b0530964a0ff8be08ceef27b47fe7e91b630ba09a243b087f4bce523add
4
- data.tar.gz: 0abb8b4cf7d274318451a094f40a48992e6759ac2d56f6a5f70fd8d18e59cd06
3
+ metadata.gz: 0312d7e0c0790b8b2a117ad6277e49bfb18c61545134b27510fa3209e951697d
4
+ data.tar.gz: 94915564bbbab8c59e139422c1a99317ec7fd5a87370e9905b16ed330cd56951
5
5
  SHA512:
6
- metadata.gz: 756394456261aca49ea0c828d9bf61996a8175da001c335ac802e06f9232641a587732765140395dc55e224a6f6d5d4facda26cdb26ddde27bb8272a6b3d09ee
7
- data.tar.gz: 2f9584331b16063497e712f4b48b20abec72e946932af5986418d8e137ffa4eb55f393e05802974a7eec47edff0d6cf84b6de6d2614b2b553f8bcad51ebeed27
6
+ metadata.gz: 7b15fc5fb4130fe0783efe8e20cf7aa2ce3962e6566d54ae5c1c85834692e5363cc3a9667eb2f7cfbf17832badc0461cbf684c1d5a5c04711e5e529d1fb11bb2
7
+ data.tar.gz: 36e154fb3fefd3dd0de59ced6d6f404e4238242ec182d78f5ced051fec01002a3af8dbe11c8eced3ff16fc978791301627fe3f06641e0ea859e9a529326ea814
@@ -37,7 +37,7 @@ that is set but empty counts as not set.
37
37
  | `jira.server` | `ARCHSIGHT_JIRA_SERVER` | Name of the Jira server as Confluence knows it (the `server` parameter of its Jira macro). Needed by the Confluence export when Confluence has more than one Jira server: without it the exported `{jira:KEY}` macros have no server and Confluence shows an error |
38
38
  | `jira.server_id` | `ARCHSIGHT_JIRA_SERVER_ID` | Id of that server (the `serverId` parameter). Copy both from the storage format of a Jira macro that already works in Confluence (page "Edit in storage format") |
39
39
  | `jira.token` | `ARCHSIGHT_JIRA_TOKEN` | Reserved, nothing uses it yet |
40
- | `import.cache_root` | `ARCHSIGHT_IMPORT_CACHE_ROOT` | Where the GitHub, GitLab and repository imports clone repositories, and what importers of the resources repository get as `cache_root` / `ARCHSIGHT_CACHE_DIR`. Default `~/.cache/archsight/git`; an import's own `import/config/cacheRoot` wins, see [Importers of the Resources Repository](/doc/import#importers-of-the-resources-repository) |
40
+ | `import.cache_root` | `ARCHSIGHT_IMPORT_CACHE_ROOT` | Where the GitHub, GitLab and repository imports clone repositories, and what importers of the resources repository get as `cache_root` / `ARCHSIGHT_CACHE_DIR`. Default `~/.cache/archsight/git`; an import's own `import/config/cacheRoot` wins, see [Importers of the Resources Repository](/doc/import#importers-of-the-resources-repository). Every import makes sure the clones are not indexed by Spotlight (macOS): an empty `.metadata_never_index` in `~/.cache/archsight`, or in the cache root when it is elsewhere |
41
41
 
42
42
  ## Command line
43
43
 
data/docs/modeling.md CHANGED
@@ -190,6 +190,9 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
190
190
  with `composedOf` (metro network → site networks, campus → halls, backbone → links).
191
191
  7. A network that supports a metro region is `locatedAt` the Location and `composedOf` the networks of its sites.
192
192
  8. Nodes attach to networks with `servedBy` and stand in facilities with `locatedAt`.
193
+ An `ApplicationInterface` is `servedBy` the networks on which it can be reached. Write them only when they are
194
+ known: an interface without a network makes no claim about its reachability, and an analysis cannot tell "not
195
+ reachable" from "not recorded".
193
196
  9. An overlay that software builds (an SDN agent wiring tenant networks on each host, an operator creating VXLAN
194
197
  segments, a load-balancer controller announcing public addresses) is `realizedBy` that `ApplicationComponent` or
195
198
  `ApplicationService` (ArchiMate cross-layer realization). This holds even when the software runs on the nodes the
@@ -546,6 +549,7 @@ Which kind to pick is in [Choosing the Kind: Infrastructure](#choosing-the-kind-
546
549
  | `TechnologyCommunicationNetwork` | `realizedBy` | `TechnologyDevice` (the switches and routers that build it) |
547
550
  | `TechnologyCommunicationNetwork` | `connects` | `TechnologyDevice` (the devices attached to it) |
548
551
  | `TechnologyNode` | `servedBy` | `TechnologyCommunicationNetwork` |
552
+ | `ApplicationInterface` | `servedBy` | `TechnologyCommunicationNetwork` (the networks on which it can be reached) |
549
553
 
550
554
  - **Underlay and overlay:** the underlay *serves* the overlay, so the overlay is `servedBy` the underlay. A BGP-EVPN
551
555
  network that carries customer workloads is one `overlay` network served by the underlay and realized by the routing
@@ -560,6 +564,10 @@ Which kind to pick is in [Choosing the Kind: Infrastructure](#choosing-the-kind-
560
564
  - **Attachment:** a node is `servedBy` the networks it is attached to; the network page lists them. A physical
561
565
  device is attached the other way round: the network `connects` it, so the network can also be `realizedBy` the
562
566
  same switch without a cycle.
567
+ - **Reachability of an interface:** an `ApplicationInterface` is `servedBy` the networks on which it can be reached,
568
+ next to the `TechnologyInterface` that carries it. With `api/authenticationMethod` this is the exposure of the
569
+ interface (see the example analysis `Analysis:Interface:ExposedWithoutAuthentication`). Leave it out when the
570
+ networks are not known.
563
571
 
564
572
  ```
565
573
  # all overlay networks
data/docs/pages.md CHANGED
@@ -339,9 +339,9 @@ confluence:
339
339
  With `drawio` off nothing draw.io-specific is written, so any Confluence shows the diagrams. Rendering a `.drawio` needs the
340
340
  draw.io desktop CLI (`drawio`, or `ARCHSIGHT_DRAWIO_CLI`) on the machine that exports; the preview of an SVG needs
341
341
  `rsvg-convert` or that CLI. A diagram that cannot be rendered fails the page instead of leaving a blank diagram. Attachments an
342
- earlier export added and the page no longer uses are removed; attachments added by others are left alone. `[[links]]` and markdown links to pages (`[text](/pages/name)`) that have a Confluence link
342
+ earlier export added and the page no longer uses are removed (a draw.io diagram is attached under its name and a hash of its content, so a changed diagram is a new attachment, because the draw.io macro would keep showing the first version of an attachment it names); attachments added by others are left alone. `[[links]]` and markdown links to pages (`[text](/pages/name)`) that have a Confluence link
343
343
  become links to them, other links to Archsight (pages without a Confluence link, resources, searches) are plain text. A page with a broken image, a diagram that does not render or an invalid
344
- ```` ```view ````/```` ```requirements ```` block is not exported and reported as failed. The Confluence title is kept. The exported page uses the Confluence layout: the page properties top left, the note that the page is generated by Archsight top right, and the content below in a section of its own.
344
+ ```` ```view ````/```` ```requirements ```` block is not exported and reported as failed. The Confluence title is kept. The exported page uses the Confluence layout: the page properties top left, the note that the page is generated by Archsight top right, and the content below in a section of its own. When the resources are a git checkout with an `origin` remote, the note names the repository and links the source file to its page on the git host (GitHub `/blob/`, GitLab `/-/blob/`, on the checked out branch, or the commit when the HEAD is detached; other hosts get only the repository link, and so does a file git does not track). User names and tokens in the remote URL never appear in the link.
345
345
 
346
346
  **Views and requirements** are live in Archsight, so Confluence gets a regular table with the data of the moment of the export (every
347
347
  export writes the current data; a page whose tables did not change is `unchanged`): `![[View/Name]]` on a line of its own, a
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "kramdown"
4
- require "kramdown-parser-gfm"
3
+ require_relative "../markdown"
5
4
  require_relative "../diagram"
6
5
 
7
6
  module Archsight
@@ -192,7 +191,7 @@ module Archsight
192
191
  # ---- markdown -> Typst
193
192
 
194
193
  def markdown(source, inline: false)
195
- root = Kramdown::Document.new(source.to_s, input: "GFM").root
194
+ root = Archsight::Markdown.document(source).root
196
195
  out = blocks(root.children)
197
196
  inline ? out.gsub(/\n\n+/, " #linebreak() ") : out
198
197
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require "kramdown"
5
- require "kramdown-parser-gfm"
4
+ require_relative "../../markdown"
6
5
 
7
6
  module Archsight
8
7
  module Export
@@ -90,8 +89,8 @@ module Archsight
90
89
  return "" if text.strip.empty?
91
90
 
92
91
  safe = text.gsub(%r{<(?!(?:https?://|mailto:))}, "&lt;")
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 />')
92
+ Archsight::Markdown.unwrapped(safe, entity_output: :as_char, auto_ids: false).to_html.strip
93
+ .gsub(%r{<(br|hr|img)([^>]*?)\s*/?>}, '<\1\2 />')
95
94
  end
96
95
 
97
96
  def h(text) = ERB::Util.html_escape(text)
@@ -7,6 +7,7 @@ require "time"
7
7
  require_relative "../../version"
8
8
  require_relative "../../assets"
9
9
  require_relative "../../helpers"
10
+ require_relative "../../git_source"
10
11
  require_relative "client"
11
12
  require_relative "credentials"
12
13
  require_relative "page_url"
@@ -117,7 +118,8 @@ module Archsight
117
118
  storage = Storage.new(page_name: page.name, source: relative_source(page),
118
119
  base_dir: Assets.base_dir_for(page, resources_dir: @resources_dir).to_s,
119
120
  resources_dir: @resources_dir, wiki: Helpers::WikiLinks.new(@database), page_id: id, base: url.base,
120
- drawio: drawio?, diagram_links: DiagramLinks.new(@database), database: @database)
121
+ drawio: drawio?, diagram_links: DiagramLinks.new(@database), database: @database,
122
+ git: GitSource.locate(page.path_ref.path.to_s))
121
123
  storage.convert(body_of(page), toc: page.annotations["page/toc"] == "yes", header: PageProperties.xml(page))
122
124
  end
123
125
 
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require "kramdown"
5
- require "kramdown-parser-gfm"
4
+ require_relative "../../markdown"
6
5
  require_relative "../../helpers/macros"
7
6
 
8
7
  module Archsight
@@ -69,7 +68,7 @@ module Archsight
69
68
  kept << live(match)
70
69
  "ARCHSIGHTKEPT#{kept.length - 1}X"
71
70
  end.gsub("<", "&lt;") # raw HTML is not allowed, it could break the storage XML
72
- html = Kramdown::Document.new(marked, input: "GFM", auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot]).to_html.strip
71
+ html = Archsight::Markdown.unwrapped(marked, auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot]).to_html.strip
73
72
  html.sub(%r{\A<p>(.*)</p>\z}m, '\1').gsub(/ARCHSIGHTKEPT(\d+)X/) { kept.fetch(Regexp.last_match(1).to_i) }
74
73
  end
75
74
 
@@ -3,8 +3,7 @@
3
3
  require "erb"
4
4
  require "uri"
5
5
  require "digest"
6
- require "kramdown"
7
- require "kramdown-parser-gfm"
6
+ require_relative "../../markdown"
8
7
  require_relative "../../assets"
9
8
  require_relative "../../diagram"
10
9
  require_relative "../../helpers/macros"
@@ -80,8 +79,9 @@ module Archsight
80
79
 
81
80
  MXFILE = "application/vnd.jgraph.mxfile"
82
81
 
83
- BANNER = "This page is generated by Archsight from <code>%<source>s</code>. " \
84
- "Changes made here are overwritten by the next export: edit the source instead."
82
+ HOMEPAGE = "https://github.com/ionos-cloud/archsight"
83
+ BANNER = "This page is generated by <a href=\"#{HOMEPAGE}\">Archsight</a> from %<source>s. " \
84
+ "Changes made here are overwritten by the next export: edit the source instead.".freeze
85
85
 
86
86
  LANGUAGES = {
87
87
  "sh" => "bash", "shell" => "bash", "zsh" => "bash", "yml" => "yaml", "json" => "javascript", "js" => "javascript",
@@ -95,6 +95,8 @@ module Archsight
95
95
 
96
96
  # @param page_name [String] used to name generated attachments
97
97
  # @param source [String] the markdown file, shown in the banner
98
+ # @param git [Archsight::GitSource::Location, nil] where the file is in its git repository: the banner then
99
+ # links it to the file in the web UI of the git host and names the repository
98
100
  # @param base_dir [String] directory of the markdown file relative to the resources directory
99
101
  # @param resources_dir [String]
100
102
  # @param wiki [Archsight::Helpers::WikiLinks] resolves [[Targets]]
@@ -103,8 +105,10 @@ module Archsight
103
105
  # @param database [Archsight::Database, nil] lets macros that name pages (pagetree) find them
104
106
  # @param diagram_links [#call, nil] resolves a diagram's `resource` references (DiagramLinks); none leaves them inert
105
107
  # @param drawio [Boolean] the Confluence has the draw.io app: diagrams go in as draw.io macros, else as images
106
- def initialize(page_name:, source:, base_dir:, resources_dir:, wiki:, page_id: nil, base: nil, drawio: false, diagram_links: nil, database: nil)
108
+ def initialize(page_name:, source:, base_dir:, resources_dir:, wiki:, page_id: nil, base: nil, drawio: false, diagram_links: nil, database: nil,
109
+ git: nil)
107
110
  @database = database
111
+ @git = git
108
112
  @page_name = page_name
109
113
  @source = source
110
114
  @base_dir = base_dir
@@ -125,7 +129,7 @@ module Archsight
125
129
  # @param header [String] storage format of the page properties, shown top left next to the banner
126
130
  # @return [Converted]
127
131
  def convert(markdown, toc: false, header: "")
128
- document = Kramdown::Document.new(protect(markdown), input: "GFM", auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot])
132
+ document = Archsight::Markdown.unwrapped(protect(markdown), auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot])
129
133
  html, = Converter.convert(document.root, document.options.merge(storage: self))
130
134
  Converted.new(body: layout(header, banner, (toc ? %(<ac:structured-macro ac:name="toc" />\n) : "") + html), attachments: @attachments, problems: @problems)
131
135
  end
@@ -226,7 +230,17 @@ module Archsight
226
230
  end
227
231
 
228
232
  def banner
229
- %(<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)
233
+ %(<ac:structured-macro ac:name="info"><ac:rich-text-body><p>#{format(BANNER, source: banner_source)}</p></ac:rich-text-body></ac:structured-macro>\n)
234
+ end
235
+
236
+ # `<code>path</code>`, in a git repository the path from its root, linked to the file on the git host, and
237
+ # the repository
238
+ def banner_source
239
+ return "<code>#{h(@source)}</code>" unless @git
240
+
241
+ file = "<code>#{h(@git.path)}</code>"
242
+ file = %(<a href="#{h(@git.url)}">#{file}</a>) if @git.url
243
+ %(#{file} in the repository <a href="#{h(@git.repository_url)}">#{h(@git.repository)}</a>)
230
244
  end
231
245
 
232
246
  # {macros}, [[Target|label]] and ![[View/Name]] become placeholders before kramdown sees them (it would split the
@@ -394,13 +408,12 @@ module Archsight
394
408
  %(<p><ac:image><ri:attachment ri:filename="#{h(filename)}" /></ac:image></p>\n)
395
409
  end
396
410
 
397
- # The diagram file of a draw.io macro and its `.png` preview, which must share the name
411
+ # The diagram file of a draw.io macro and its `.png` preview, which must share the name. The name carries a hash
412
+ # of the content: the macro shows revision 1 of the attachment it names, so a changed diagram uploaded under the
413
+ # old name (as a new version of it) would not be shown. A new name is a new attachment, the old one is removed.
398
414
  # @return [String] the name the macro refers to
399
415
  def attach_diagram(stem, xml, png)
400
- sha = Digest::SHA256.hexdigest(xml)
401
- name = stem
402
- taken = ->(file, data) { @attachments.key?(file) && @attachments[file].data != data }
403
- name = "#{stem}-#{sha[0, 8]}" if taken.call(stem, xml) || taken.call("#{stem}.png", png)
416
+ name = "#{stem}-#{Digest::SHA256.hexdigest(xml)[0, 8]}"
404
417
  @attachments[name] = Attachment.new(filename: name, data: xml, content_type: MXFILE)
405
418
  @attachments["#{name}.png"] = Attachment.new(filename: "#{name}.png", data: png, content_type: "image/png")
406
419
  name
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require "kramdown"
5
- require "kramdown-parser-gfm"
4
+ require_relative "../../markdown"
6
5
  require_relative "../../view_table"
7
6
  require_relative "../../helpers/macros"
8
7
  require_relative "diagram_links"
@@ -67,7 +66,7 @@ module Archsight
67
66
  return "" if text.to_s.strip.empty?
68
67
 
69
68
  safe = text.gsub(%r{<(?!(?:https?://|mailto:))}, "&lt;")
70
- html = Kramdown::Document.new(safe, input: "GFM", entity_output: :as_char, auto_ids: false).to_html.strip
69
+ html = Archsight::Markdown.unwrapped(safe, entity_output: :as_char, auto_ids: false).to_html.strip
71
70
  html.match(%r{\A<p>((?:(?!</?p>).)*)</p>\z}m) ? Regexp.last_match(1) : html
72
71
  end
73
72
 
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "open3"
5
+ require "pathname"
6
+ require "uri"
7
+
8
+ module Archsight
9
+ # Where a file lives in its git repository and its page in the web UI of the git host, for links to the source of
10
+ # generated content (the banner of an exported Confluence page). Only local git is asked (the repository of the
11
+ # file, its `origin` remote, the checked out branch), nothing goes over the network.
12
+ #
13
+ # The web URL is `https://<host>/<owner>/<repo>` of the remote, without the user, password or token a remote URL
14
+ # may carry. GitHub files are `/blob/<ref>/<path>`, GitLab files `/-/blob/<ref>/<path>`; on other hosts, and for a
15
+ # file git does not track, only the repository is linked. The ref is the checked out branch, the commit when the
16
+ # HEAD is detached (CI).
17
+ module GitSource
18
+ # repository: "owner/repo"; repository_url: its web page; path: the file relative to the repository root;
19
+ # url: the web page of the file, nil if the host is not known
20
+ Location = Struct.new(:repository, :repository_url, :path, :url, keyword_init: true)
21
+
22
+ module_function
23
+
24
+ # @param file [String] path of a file
25
+ # @return [Location, nil] nil if the file is not in a git repository with an `origin` remote
26
+ def locate(file)
27
+ dir = File.dirname(File.expand_path(file))
28
+ root = git(dir, "rev-parse", "--show-toplevel") or return nil
29
+ remote = git(dir, "remote", "get-url", "origin") or return nil
30
+ host, repository = host_and_repository(remote)
31
+ return nil unless host && repository
32
+
33
+ path = Pathname.new(File.realpath(file)).relative_path_from(Pathname.new(File.realpath(root))).to_s
34
+ ref = git(dir, "symbolic-ref", "--short", "-q", "HEAD") || git(dir, "rev-parse", "HEAD")
35
+ repository_url = "https://#{host}/#{repository}"
36
+ url = tracked?(root, path) ? file_url(host, repository_url, ref, path) : nil
37
+ Location.new(repository: repository, repository_url: repository_url, path: path, url: url)
38
+ rescue SystemCallError, ArgumentError
39
+ nil
40
+ end
41
+
42
+ # `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo.git`, `https://user:token@host/owner/repo` ->
43
+ # ["host", "owner/repo"]; the port of an ssh remote is not the port of the web UI and is left out
44
+ def host_and_repository(remote)
45
+ if (scp = remote.match(%r{\A(?:[^@/\s]+@)?([^:/\s]+):(?!//)(.+)\z}))
46
+ host, path = scp.captures
47
+ else
48
+ uri = URI.parse(remote)
49
+ return [nil, nil] unless uri.host && %w[http https ssh git].include?(uri.scheme)
50
+
51
+ host = uri.scheme.start_with?("http") && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
52
+ path = uri.path
53
+ end
54
+ repository = path.delete_prefix("/").delete_suffix("/").delete_suffix(".git")
55
+ repository.empty? ? [nil, nil] : [host, repository]
56
+ rescue URI::InvalidURIError
57
+ [nil, nil]
58
+ end
59
+
60
+ def file_url(host, repository_url, ref, path)
61
+ blob = if host.match?(/(\A|\.)github\./) then "blob"
62
+ elsif host.match?(/(\A|\.)gitlab\./) then "-/blob"
63
+ end
64
+ return nil unless blob && ref
65
+
66
+ "#{repository_url}/#{blob}/#{escape(ref)}/#{escape(path)}"
67
+ end
68
+
69
+ # An untracked or ignored file is not on the git host: no link that leads nowhere
70
+ def tracked?(root, path) = !git(root, "ls-files", "--", path).nil?
71
+
72
+ # each segment escaped, the slashes kept
73
+ def escape(path) = path.split("/").map { |segment| ERB::Util.url_encode(segment) }.join("/")
74
+
75
+ def git(dir, *)
76
+ out, _err, status = Open3.capture3("git", "-C", dir, *)
77
+ value = out.strip
78
+ status.success? && !value.empty? ? value : nil
79
+ end
80
+ end
81
+ end
@@ -2,8 +2,7 @@
2
2
 
3
3
  require "cgi"
4
4
  require "erb"
5
- require "kramdown"
6
- require "kramdown-parser-gfm"
5
+ require_relative "../markdown"
7
6
  require_relative "../assets"
8
7
 
9
8
  module Archsight
@@ -52,7 +51,7 @@ module Archsight
52
51
  # Problems with the image references of a markdown text.
53
52
  # @return [Array<Hash>] `{ reference:, status: :outside|:type|:missing, path: }`
54
53
  def audit(markdown, base_dir:, resources_dir:)
55
- html = Kramdown::Document.new(markdown.to_s, input: "GFM").to_html
54
+ html = Archsight::Markdown.document(markdown).to_html
56
55
  html.scan(IMG).filter_map do |tag|
57
56
  reference = attribute(tag, SRC)
58
57
  next if reference.nil? || Assets.external?(reference)
@@ -65,7 +64,7 @@ module Archsight
65
64
  # The existing .asd files a markdown text embeds (for the linter)
66
65
  # @return [Array<Array(String, String)>] normalized asset path and real file
67
66
  def asd_files(markdown, base_dir:, resources_dir:)
68
- html = Kramdown::Document.new(markdown.to_s, input: "GFM").to_html
67
+ html = Archsight::Markdown.document(markdown).to_html
69
68
  html.scan(IMG).filter_map do |tag|
70
69
  reference = attribute(tag, SRC)
71
70
  next if reference.nil? || Assets.external?(reference)
@@ -442,6 +442,7 @@ class Archsight::Import::Executor
442
442
  progress: import_progress,
443
443
  shared_writer: @shared_writer
444
444
  )
445
+ Archsight::Import.never_index(handler.cache_root)
445
446
  handler.execute
446
447
  remove_stale_outputs(import, handler.owned_outputs)
447
448
  @mutex.synchronize { @ran_this_run.add(import.name) }
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+
3
5
  # Define import module namespaces
4
6
  module Archsight
5
7
  module Import
@@ -13,8 +15,31 @@ module Archsight
13
15
  # expand it before use)
14
16
  def self.cache_root(override = nil)
15
17
  require "archsight/user_config"
16
- override || Archsight::UserConfig.setting("import", "cache_root")&.to_s ||
17
- File.join(Dir.home, ".cache", "archsight", "git")
18
+ override || Archsight::UserConfig.setting("import", "cache_root")&.to_s || File.join(cache_dir, "git")
19
+ end
20
+
21
+ # The cache directory of Archsight, ~/.cache/archsight
22
+ def self.cache_dir = File.join(Dir.home, ".cache", "archsight")
23
+
24
+ # Tells Spotlight (macOS) not to index this
25
+ NEVER_INDEX = ".metadata_never_index"
26
+
27
+ # Makes sure the clones are not indexed by Spotlight: the marker file in ~/.cache/archsight, or in the cache root
28
+ # itself when it is somewhere else. Run before every import; a directory that cannot be written is left alone,
29
+ # the import does not depend on it.
30
+ # @param root [String] the cache root (Handler#cache_root)
31
+ # @return [String, nil] the marker file, nil if it could not be written
32
+ def self.never_index(root)
33
+ dir = File.expand_path(root)
34
+ dir = cache_dir if dir.start_with?("#{cache_dir}/")
35
+ marker = File.join(dir, NEVER_INDEX)
36
+ return marker if File.exist?(marker)
37
+
38
+ FileUtils.mkdir_p(dir)
39
+ FileUtils.touch(marker)
40
+ marker
41
+ rescue SystemCallError
42
+ nil
18
43
  end
19
44
 
20
45
  # The entries of `import/dependsOn`: import names and glob patterns
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "kramdown"
3
+ require_relative "markdown"
4
4
  require_relative "helpers"
5
5
  require_relative "linter/structure"
6
6
 
@@ -319,7 +319,7 @@ module Archsight
319
319
  return if instance.annotations.key?("generated/script")
320
320
 
321
321
  begin
322
- Kramdown::Document.new(value, input: "GFM")
322
+ Archsight::Markdown.document(value)
323
323
  rescue StandardError => e
324
324
  @errors << "#{instance.path_ref}: Markdown syntax error in annotation '#{key}': #{e.message}"
325
325
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kramdown"
4
+ require "kramdown-parser-gfm"
5
+
6
+ module Archsight
7
+ # The one way markdown is parsed, for the web, the export, the linter and the analysis output alike. GFM, but a
8
+ # single newline inside a paragraph is a space, as in CommonMark and GitHub's rendering of `.md` files (the GFM
9
+ # parser of kramdown would make it a `<br />`, as GitHub does in comments). A line break is two trailing spaces,
10
+ # a trailing backslash or `<br>`.
11
+ module Markdown
12
+ OPTIONS = { input: "GFM", hard_wrap: false }.freeze
13
+
14
+ # Raw HTML whose text is kept as it is
15
+ VERBATIM = %w[pre code script style textarea].freeze
16
+
17
+ def self.document(text, **)
18
+ Kramdown::Document.new(text.to_s, **OPTIONS, **)
19
+ end
20
+
21
+ # The document without the newlines of the source in its text, for a target that keeps a newline instead of
22
+ # reading it as a space (Confluence): a soft line break becomes a space, the newline after a line break goes.
23
+ # Inline code over two lines is one line (as in CommonMark). Paragraphs, breaks and code blocks are unchanged.
24
+ def self.unwrapped(text, **)
25
+ document(text, **).tap { |doc| unwrap(doc.root) }
26
+ end
27
+
28
+ def self.unwrap(element)
29
+ element.children.each_with_index do |child, index|
30
+ if child.type == :text
31
+ previous = index.positive? ? element.children[index - 1] : nil
32
+ joined = child.value.gsub(/[ \t]*\r?\n[ \t]*/, " ")
33
+ child.value = line_break?(previous) ? joined.lstrip : joined
34
+ elsif child.type == :codespan
35
+ child.value = child.value.gsub(/\r?\n/, " ")
36
+ elsif !(child.type == :html_element && VERBATIM.include?(child.value))
37
+ unwrap(child)
38
+ end
39
+ end
40
+ end
41
+ private_class_method :unwrap
42
+
43
+ def self.line_break?(element)
44
+ element && (element.type == :br || (element.type == :html_element && element.value == "br"))
45
+ end
46
+ private_class_method :line_break?
47
+ end
48
+ end