archsight 0.4.2 → 0.5.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/Dockerfile +4 -0
- data/docs/configuration.md +5 -0
- data/docs/diagram.md +69 -0
- data/docs/index.md.erb +1 -0
- data/docs/modeling.md +3 -1
- data/docs/pdf-export.md +118 -0
- data/lib/archsight/analysis/typst_renderer.rb +25 -78
- data/lib/archsight/cli.rb +39 -1
- data/lib/archsight/database.rb +4 -3
- data/lib/archsight/diagram/cli.rb +11 -1
- data/lib/archsight/diagram/errors.rb +1 -0
- data/lib/archsight/diagram/graph/attributes.rb +3 -1
- data/lib/archsight/diagram/graph/node.rb +18 -1
- data/lib/archsight/diagram/includes.rb +168 -0
- data/lib/archsight/diagram/layout/expander.rb +75 -3
- data/lib/archsight/diagram/layout/rank_arranger.rb +3 -1
- data/lib/archsight/diagram/parser/ast.rb +5 -0
- data/lib/archsight/diagram/parser/table_layout.rb +2 -0
- data/lib/archsight/diagram/parser.rb +58 -16
- data/lib/archsight/diagram.rb +20 -4
- data/lib/archsight/export/confluence/storage.rb +6 -1
- data/lib/archsight/helpers/diagram_blocks.rb +34 -19
- data/lib/archsight/helpers/diagram_includes.rb +67 -0
- data/lib/archsight/helpers.rb +1 -0
- data/lib/archsight/linter.rb +10 -2
- data/lib/archsight/markdown.rb +17 -1
- data/lib/archsight/pdf/cache.rb +41 -0
- data/lib/archsight/pdf/compiler.rb +69 -0
- data/lib/archsight/pdf/dependency_graph.rb +127 -0
- data/lib/archsight/pdf/document.rb +227 -0
- data/lib/archsight/pdf/embeds.rb +134 -0
- data/lib/archsight/pdf/images.rb +94 -0
- data/lib/archsight/pdf/markdown.rb +217 -0
- data/lib/archsight/pdf/remote_image.rb +122 -0
- data/lib/archsight/pdf/resource_sheet.rb +129 -0
- data/lib/archsight/pdf/sample_document.rb +67 -0
- data/lib/archsight/pdf/tables.rb +72 -0
- data/lib/archsight/pdf/theme.rb +125 -0
- data/lib/archsight/pdf/typst/archsight.typ +152 -0
- data/lib/archsight/pdf/typst.rb +45 -0
- data/lib/archsight/pdf.rb +30 -0
- data/lib/archsight/references.rb +4 -3
- data/lib/archsight/resources/application_component.rb +10 -0
- data/lib/archsight/resources/application_service.rb +9 -0
- data/lib/archsight/resources/config.rb +147 -0
- data/lib/archsight/resources/technology_service.rb +9 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/json_helpers.rb +3 -2
- data/lib/archsight/web/api/openapi/spec.yaml +239 -2
- data/lib/archsight/web/api/page_helpers.rb +1 -1
- data/lib/archsight/web/api/pdf_helpers.rb +46 -0
- data/lib/archsight/web/api/routes.rb +34 -3
- data/lib/archsight/web/application.rb +2 -1
- data/lib/archsight/web/public/vue/DocPage-D3QtnrmO.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-lZyG6dVt.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage-BhdYGZJk.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-CoqAjFdR.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-B7SaXVeb.js +2 -0
- data/lib/archsight/web/public/vue/{InstanceRouter-Ca_C8izS.css → InstanceRouter-BCKbW-Wp.css} +1 -1
- data/lib/archsight/web/public/vue/InstanceRouter-DJJn9fhP.js +1 -0
- data/lib/archsight/web/public/vue/KindList-UqMajqbf.js +1 -0
- data/lib/archsight/web/public/vue/PageView-CirIXjW6.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-BIJH6gQC.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-761vG1Jw.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-BPHa6yhK.js +1 -0
- data/lib/archsight/web/public/vue/WikiPage-Bi7dzY2h.js +3 -0
- data/lib/archsight/web/public/vue/{WikiPage-TpmM1VAF.css → WikiPage-CuUi4C8v.css} +1 -1
- data/lib/archsight/web/public/vue/{index-yiZbbvAs.js → index-BF4zUlWZ.js} +2 -2
- data/lib/archsight/web/public/vue/index-BO59Fbpf.css +1 -0
- data/lib/archsight/web/public/vue.html +2 -2
- metadata +37 -17
- data/lib/archsight/web/public/vue/DocPage-C8Ysway-.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-pr2MR6_b.js +0 -35
- data/lib/archsight/web/public/vue/ErrorPage-FAkpW-hn.js +0 -2
- data/lib/archsight/web/public/vue/GraphView-liQJzFyo.js +0 -1
- data/lib/archsight/web/public/vue/HomePage-DYpzSXG8.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-BdkBTmGO.js +0 -1
- data/lib/archsight/web/public/vue/KindList-CrgI4qDb.js +0 -1
- data/lib/archsight/web/public/vue/PageView-BIxT_t49.js +0 -1
- data/lib/archsight/web/public/vue/QueryError-ChdyV8B1.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-DdtaHY4k.js +0 -2
- data/lib/archsight/web/public/vue/SearchResults-BYRAcK_o.js +0 -1
- data/lib/archsight/web/public/vue/WikiPage-CgvRN6R2.js +0 -3
- data/lib/archsight/web/public/vue/index-BLSE5DSv.css +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7ada570ca5a5647d0cb9dc9f23b37d8adc9c0b6eff5420154c8ef3d30d9ea95
|
|
4
|
+
data.tar.gz: ba2e28e035b6d091fbfaeef71b0bc8d775f84c88541ecea0de86ba7d256bde42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 739c46f505dfa73125839be21c2f0baaba7b4ba2a98926ca449a2cf87088cbc2c910c6b016c67ac7905540430927f94f2dc1b0bdce29735aec1ab29857390d7c
|
|
7
|
+
data.tar.gz: c259164f01f0d14e47dc5bf7f25a572a1a519dcc92ca788f3191274df5b6e0fd4d3d90fcdc5bc253bca6b454db464aa6ab46eaffa1f5a7f515b000a307506324
|
data/Dockerfile
CHANGED
|
@@ -25,6 +25,10 @@ ENV GEM_HOME=/usr/local/bundle
|
|
|
25
25
|
ENV GEM_PATH=/usr/local/bundle:/usr/local/lib/ruby/gems/4.0.0
|
|
26
26
|
ENV PATH="/usr/local/bundle/bin:${PATH}"
|
|
27
27
|
|
|
28
|
+
# PDF export (Archsight::Pdf): typst typesets the documents, rsvg-convert turns the diagrams into vector graphics,
|
|
29
|
+
# graphviz draws the dependency graph of a resource
|
|
30
|
+
RUN apk add --no-cache typst rsvg-convert font-liberation font-dejavu font-noto-emoji graphviz
|
|
31
|
+
|
|
28
32
|
RUN mkdir -p /resources
|
|
29
33
|
|
|
30
34
|
ENV ARCHSIGHT_RESOURCES_DIR=/resources
|
data/docs/configuration.md
CHANGED
|
@@ -37,6 +37,11 @@ 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
|
+
| `pdf.typst` | `ARCHSIGHT_PDF_TYPST` | Path of the `typst` command that typesets the [PDF export](/doc/pdf-export), if it is not on the `PATH` |
|
|
41
|
+
| `pdf.dot` | `ARCHSIGHT_PDF_DOT` | Path of Graphviz' `dot` command that draws the dependency graph in the [PDF export](/doc/pdf-export), if it is not on the `PATH`. Without it the PDF of a resource has no graph |
|
|
42
|
+
| `pdf.timeout` | `ARCHSIGHT_PDF_TIMEOUT` | Seconds a PDF may take to typeset before it is cancelled. Default `60` |
|
|
43
|
+
| `pdf.remote_images` | `ARCHSIGHT_PDF_REMOTE_IMAGES` | `false` stops the PDF export from fetching images that a page embeds by URL (default: fetched, public hosts only) |
|
|
44
|
+
| `pdf.max_analyses` | `ARCHSIGHT_PDF_MAX_ANALYSES` | How many `![[Analysis/...]]` embeds one PDF may run (each can take as long as its own timeout). Default `5`; further ones are shown as a note |
|
|
40
45
|
| `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
46
|
|
|
42
47
|
## Command line
|
data/docs/diagram.md
CHANGED
|
@@ -43,6 +43,7 @@ archsight diagram three_tier.asd --relation=all # also show control/data-flow
|
|
|
43
43
|
archsight diagram overview.asd --theme=compact # denser spacing + smaller fonts (or cozy, tight)
|
|
44
44
|
archsight diagram overview.asd --legend=bottom # legend below (or right/left/top/none; default auto)
|
|
45
45
|
archsight diagram *.asd # render every file to its own .svg
|
|
46
|
+
archsight diagram app.asd --resources=model/ # resolve include \"file://...\" paths from this directory
|
|
46
47
|
archsight diagram --format *.asd # re-indent the sources in place
|
|
47
48
|
archsight diagram --format --check *.asd # list unformatted sources, exit 1 if any (CI)
|
|
48
49
|
```
|
|
@@ -144,6 +145,11 @@ hint for near misses), never silently ignored.
|
|
|
144
145
|
| `link` | nodes, containers | `http(s)://…`, `mailto:…` or a relative URL |
|
|
145
146
|
| `resource` | nodes, containers | a resource name (`Archsight:Web`) or `Kind/Name` (`ApplicationService/Archsight:Web`); see *Linking to resources* |
|
|
146
147
|
| `extend` | nodes, containers | `true`, `false`; containers also `height` |
|
|
148
|
+
| `anchor` | containers | `top`, `bottom`, `left`, `right`, `center` |
|
|
149
|
+
|
|
150
|
+
`extend "height"` on a direct child of a `layer` grows it to its tallest sibling. Inside such a box (also through a `boundary` around a `stack`), a `stack` child carrying `extend "height"` takes the extra height and moves the ranks below it down, so e.g. a footer rank lines up across the columns. The child can be a `layer` too; its own `extend "height"` children then fill the layer's new height.
|
|
151
|
+
|
|
152
|
+
`anchor` says where a container's content sits when its box grows: `bottom` (or `top`) when it is made taller, `right` (or `left`) when it is made wider, `center` for the middle. Default: top for `extend "height"`, centered otherwise. It is not inherited, and has no effect on a box whose room is taken by a stack child with `extend "height"`.
|
|
147
153
|
| `shape` | leaves | `rectangle`, `circle`, `cylinder`, `pipe`, `actor`, `file`, `module` |
|
|
148
154
|
| `gap` | containers | pixels (`40`), a percentage (`150%`) or a signed one (`+20%`, `-50%`) |
|
|
149
155
|
| `ranks` | containers | `auto`, `on`, `down`, `right`, `off` |
|
|
@@ -298,6 +304,69 @@ no broken styling. Library users pass a `resolver:` to `Diagram.render`
|
|
|
298
304
|
(any `call(reference)` returning a URL, `nil` for a resource that exists but has nowhere to link to (the node stays as it is), `:missing` or `:ambiguous`, see
|
|
299
305
|
`Archsight::Helpers::ResourceResolver`) and, to collect failures, `unresolved: []`.
|
|
300
306
|
|
|
307
|
+
## Including diagrams
|
|
308
|
+
|
|
309
|
+
`include "<path>"` splices another diagram in at that position, so a part
|
|
310
|
+
that several diagrams share (a platform, a network, a team's services) is
|
|
311
|
+
written once:
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
group "app" {
|
|
315
|
+
component "web" { label "Web server" }
|
|
316
|
+
stack {
|
|
317
|
+
include "archsight://ApplicationComponent/Platform:Database"
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
include "file://shared/network.asd"
|
|
321
|
+
web -> db
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The options of an `include` go in braces after the path (`namespace`, see below).
|
|
325
|
+
|
|
326
|
+
| Path | Included diagram |
|
|
327
|
+
|------|------------------|
|
|
328
|
+
| `archsight://<Kind>/<name>` | the `architecture/diagram` annotation of that resource |
|
|
329
|
+
| `file://<path>` | an `.asd` file, relative to the resources directory (also in nested includes) |
|
|
330
|
+
|
|
331
|
+
A path that leaves the resources directory (`..`, absolute paths, symlinks
|
|
332
|
+
pointing out) or is not an `.asd` file is refused. An `include` stands
|
|
333
|
+
wherever a `layer` or `stack` can: at the top level or inside a container
|
|
334
|
+
(not inside a leaf like `component`, and not together with `columns`). Included
|
|
335
|
+
diagrams may include others (up to 8 levels, 100 includes in all; a cycle is an
|
|
336
|
+
error). Nodes are one namespace across all of it, so an id defined twice is an
|
|
337
|
+
error, and an edge in either part may name a node of the other.
|
|
338
|
+
|
|
339
|
+
### Including a diagram more than once
|
|
340
|
+
|
|
341
|
+
Ids are one namespace, so including the same diagram twice would define
|
|
342
|
+
every node twice. A `namespace` puts `<namespace>.` in front of every id the
|
|
343
|
+
included diagram defines (nodes and dataflows, and the references to them inside
|
|
344
|
+
it):
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
include "archsight://ApplicationComponent/Platform:Server" { namespace "a" }
|
|
348
|
+
include "archsight://ApplicationComponent/Platform:Server" { namespace "b" }
|
|
349
|
+
a.web -> b.db
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
A node without a `label` keeps showing its own id (`web`, not `a.web`), and the same
|
|
353
|
+
id from different includes no longer clashes. A reference inside the included diagram to an id it
|
|
354
|
+
does not define names a node of the including diagram and stays as it is. Namespaces
|
|
355
|
+
nest: a diagram included with `namespace "o"` that itself includes one with `namespace "in"` has
|
|
356
|
+
`o.in.…` ids. A namespace is a name of letters, digits, `_` and `-`, joined by dots.
|
|
357
|
+
|
|
358
|
+
Dataflows and `theme`/`legend`/`ranks` apply to the whole diagram: an included
|
|
359
|
+
diagram holding them keeps them when it is included at the top level, and they are
|
|
360
|
+
ignored, with a warning, when it is included inside a container. The warning is
|
|
361
|
+
written to the server log and listed by `archsight lint`.
|
|
362
|
+
|
|
363
|
+
A missing resource or file, a bad path or a broken included diagram fails the
|
|
364
|
+
diagram with an error that names the include (`archsight lint` reports it). Standalone
|
|
365
|
+
`archsight diagram` takes `file://` paths relative to `--resources DIR` (default:
|
|
366
|
+
the directory of the input) and has no `archsight://`. Library users pass `includes:` to
|
|
367
|
+
`Diagram.render`, any `call(path)` returning the source or `:missing`, `:forbidden` or `:invalid`
|
|
368
|
+
(see `Archsight::Helpers::DiagramIncludes`), and `warnings: []` to collect the warnings.
|
|
369
|
+
|
|
301
370
|
## Diagrams in markdown
|
|
302
371
|
|
|
303
372
|
A fenced code block tagged `asd` in any markdown the web UI renders (docs,
|
data/docs/index.md.erb
CHANGED
|
@@ -8,6 +8,7 @@ Welcome to the Architecture Documentation System. This documentation covers how
|
|
|
8
8
|
- [Configuration](/doc/configuration) - The configuration file and environment variables for tokens and URLs
|
|
9
9
|
- [Query Syntax](/doc/search) - Search and filter resources
|
|
10
10
|
- [Wiki Pages](/doc/pages) - Author markdown pages and arrange them in the sidebar tree
|
|
11
|
+
- [PDF Export](/doc/pdf-export) - Download pages and resources as PDF, with a Config for the look
|
|
11
12
|
- [Architecture Modeling](/doc/modeling) - How to model your architecture
|
|
12
13
|
- [REST API Documentation](/docs/api) - JSON API for programmatic access
|
|
13
14
|
- [Diagrams](/doc/diagram) - Render `.asd` diagram files to SVG
|
data/docs/modeling.md
CHANGED
|
@@ -36,7 +36,7 @@ There are two families of verbs:
|
|
|
36
36
|
|
|
37
37
|
| Family | Verbs | Written on | Points to |
|
|
38
38
|
|--------|-------|------------|-----------|
|
|
39
|
-
| Responsibility and provider (down) | `performedBy`, `ownedBy`, `executedBy`, `servedBy`, `guidedBy`, `maintainedBy`, `contributedBy`, `hasConcern`, `representedBy`, `realizedThrough`, `exposes`, `provisions`, `connects`, `mitigatedBy`, `influences`, `triggers`, `affects`, `assesses`, `contains`, `closedBy`, `compares` | the element that needs, uses or is concerned with something | what provides it or is responsible for it |
|
|
39
|
+
| Responsibility and provider (down) | `performedBy`, `ownedBy`, `operatedBy`, `executedBy`, `servedBy`, `guidedBy`, `maintainedBy`, `contributedBy`, `hasConcern`, `representedBy`, `realizedThrough`, `exposes`, `provisions`, `connects`, `mitigatedBy`, `influences`, `triggers`, `affects`, `assesses`, `contains`, `closedBy`, `compares` | the element that needs, uses or is concerned with something | what provides it or is responsible for it |
|
|
40
40
|
| Realization (up) | `realizes`, `partiallyRealizes`, `plans`, `satisfies`, `evidencedBy` (on the evidenced element), `provides` | the concrete element | the abstract element it answers to (requirement, goal, capability, plateau) |
|
|
41
41
|
|
|
42
42
|
Rules:
|
|
@@ -616,6 +616,8 @@ Relations are written on the element that is placed (see [Direction of Relations
|
|
|
616
616
|
| `TechnologyFacility` | `composedOf` | `TechnologyFacility` (nested sites, the rooms of a data center) |
|
|
617
617
|
| `TechnologyFacility` | `servedBy` | `BusinessActor` (the operator) |
|
|
618
618
|
| `TechnologyDevice`, `TechnologyEquipment`, `TechnologyNode`, `TechnologyCommunicationNetwork` | `ownedBy` | `BusinessActor`, `BusinessRole` (the team that operates it) |
|
|
619
|
+
| `ApplicationComponent`, `ApplicationService`, `TechnologyService` | `operatedBy` | `BusinessActor`, `BusinessRole` (the team that runs it in production) |
|
|
620
|
+
| `ApplicationComponent`, `ApplicationService`, `TechnologyService` | `maintainedBy` | `BusinessActor` (the team that builds and changes it) |
|
|
619
621
|
| `TechnologyDevice`, `TechnologyEquipment`, `TechnologyNode` | `servedBy` | `BusinessActor` (the team it is dedicated to, e.g. a test partition) |
|
|
620
622
|
|
|
621
623
|
`locatedAt` with many targets is the point: a service that runs in three data centers is **one** service with three
|
data/docs/pdf-export.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# PDF Export
|
|
2
|
+
|
|
3
|
+
Every [page](/doc/pages) and every resource can be downloaded as a PDF. The document is typeset by [Typst](https://typst.app),
|
|
4
|
+
so the server (or the machine running the CLI) needs the `typst` command; the container image has it, together with
|
|
5
|
+
`rsvg-convert` (turns diagrams into vector graphics), `dot` of Graphviz (draws the dependency graph) and a few fonts.
|
|
6
|
+
|
|
7
|
+
## Getting a PDF
|
|
8
|
+
|
|
9
|
+
- **Web UI:** the **PDF** button next to **Edit** on a page or resource.
|
|
10
|
+
- **API:** `GET /api/v1/pages/{name}/pdf` and `GET /api/v1/kinds/{kind}/instances/{name}/pdf`, see the [REST API documentation](/docs/api).
|
|
11
|
+
The response has an `ETag` that changes whenever the page, an image, a diagram or the template changes.
|
|
12
|
+
Query parameters: `template` (name of a pdf Config) and `draft=1` (watermark).
|
|
13
|
+
- **CLI:** `archsight pdf <page>` or `archsight pdf Kind/Name`, with `-o file.pdf`, `--template` and `--draft`.
|
|
14
|
+
|
|
15
|
+
Without `typst` the API answers `503` and the button shows the reason.
|
|
16
|
+
|
|
17
|
+
## What is in the document
|
|
18
|
+
|
|
19
|
+
- A **page** is typeset as written: headings, text, tables (with their alignment), task lists, code, images next to the page,
|
|
20
|
+
`asd` diagrams (as vector graphics), `[[links]]` (as their label), `{status:...}` (a coloured box), `{jira:KEY}` (a link
|
|
21
|
+
when `jira.issue_url` is set) and `{emoticon:...}`. Macros that need the running application (`{children}`) are left out.
|
|
22
|
+
Mermaid blocks and draw.io images cannot be drawn on the server: a note stands there (and the Mermaid source).
|
|
23
|
+
- **Tables** have horizontal rules only: a stronger one under the header row and a light one under every other row, no frame
|
|
24
|
+
and no vertical lines, in the muted colour of the Config; the first column is flush with the text.
|
|
25
|
+
- **Live content is the data of the moment of the export**, as in the Confluence export:
|
|
26
|
+
- `![[View/Name]]` and ```` ```view ```` blocks are tables (title, number of items, repeated header row; long columns
|
|
27
|
+
share the width), `` ```requirements `` blocks are the requirements table.
|
|
28
|
+
- `![[Analysis/Name]]` runs the analysis and puts its sections (headings, text, tables, lists, messages) into the document.
|
|
29
|
+
An analysis runs for as long as its own `analysis/timeout` allows, so at most 5 run per document (`pdf.max_analyses`),
|
|
30
|
+
and a result is reused for a minute. A failed analysis shows its error.
|
|
31
|
+
- `![[Kind/Name]]` of another kind is the resource's description.
|
|
32
|
+
- Only an embed on a line of its own (or several, one per line) is content; inside a sentence it stays its name.
|
|
33
|
+
- What cannot be shown (unknown or ambiguous reference, an invalid view or requirements block) is a visible note in the
|
|
34
|
+
document, never blank space. Embeds inside an embedded description are not expanded again.
|
|
35
|
+
- **Images by URL** (``) are fetched by the server so they can go into the PDF. Only public hosts on https/http
|
|
36
|
+
are contacted (never loopback, private or link-local addresses, so the server cannot be used to reach the network
|
|
37
|
+
behind it), at most 10 MB and 10 seconds each, and only real png, jpeg, gif or webp files are used; otherwise the alt text
|
|
38
|
+
is shown. `pdf.remote_images: false` turns this off.
|
|
39
|
+
- Any other **resource** becomes a generated document: its description, its properties, its relations and its
|
|
40
|
+
`architecture/diagram`, and its **dependency graph**, shown first, as on its page (the resources its relations lead to, two steps
|
|
41
|
+
deep, following its own layer and the one below it, nodes coloured by layer), drawn by `dot` in the direction, left to
|
|
42
|
+
right or top to bottom, in which it is larger on the page. A resource without
|
|
43
|
+
relations has no graph, and without `dot` the document is made without it. Properties are a label and value list (related annotations kept together; lists as "a, b";
|
|
44
|
+
multi-line and markdown values as sections of their own). Relations are shown in both directions, **Outgoing** (what this
|
|
45
|
+
resource points to) and **Incoming** (what points to it, read from the other side): the relation is named once, with the
|
|
46
|
+
kind next to it and the resources flowing in two columns, so a resource with many relations stays readable.
|
|
47
|
+
|
|
48
|
+
## Steering the layout with tags
|
|
49
|
+
|
|
50
|
+
Tags of the page (`tags:` in the frontmatter) or of a resource (`architecture/tags`):
|
|
51
|
+
|
|
52
|
+
| Tag | Effect |
|
|
53
|
+
|-----|--------|
|
|
54
|
+
| `pdf:cover` | A cover page (logo band, title, id, version, date, owner) instead of the title block |
|
|
55
|
+
| `pdf:toc` | A table of contents |
|
|
56
|
+
| `pdf:landscape` | Landscape pages |
|
|
57
|
+
| `pdf:draft` | A "draft" watermark on every page |
|
|
58
|
+
| `pdf:no-graph` | Leave the dependency graph out of a resource's document |
|
|
59
|
+
| `pdf:template:<name>` | Use the pdf Config `<name>` instead of `default` |
|
|
60
|
+
| `version:<x>` | The document version in the header (else the page property `Version`, else `1`) |
|
|
61
|
+
|
|
62
|
+
Further page data used by the layout: `title`, `owner` (else `author`), `updated` (else today), and the page properties
|
|
63
|
+
`Document ID` (default `Kind/name`) and `Classification` (red marking in the footer, default from the template).
|
|
64
|
+
|
|
65
|
+
## The look: Config
|
|
66
|
+
|
|
67
|
+
A `Config` resource (layer *Other*) holds settings of a feature of the tool, kept with the model; `config/type` says
|
|
68
|
+
which feature. The PDF export reads the Configs of type `pdf`: colours, fonts, logo and the text around the pages. The one
|
|
69
|
+
named `default` is used for every document that does not name another (`pdf:template:<name>`); `brand`, `PDF:brand` and
|
|
70
|
+
`Config:PDF:brand` all name the same Config. Without any a neutral look is used. A Config of another type is never used
|
|
71
|
+
for the PDF, so every feature can have its own `default`. (The per-user file with tokens and URLs stays
|
|
72
|
+
[`~/.config/archsight/archsight.yaml`](/doc/configuration).)
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
apiVersion: architecture/v1alpha1
|
|
76
|
+
kind: Config
|
|
77
|
+
metadata:
|
|
78
|
+
name: Config:PDF:default
|
|
79
|
+
annotations:
|
|
80
|
+
config/type: pdf
|
|
81
|
+
pdf/color-primary: "#1f3a5f"
|
|
82
|
+
pdf/color-accent: "#2f7fd1"
|
|
83
|
+
pdf/logo: assets/logo.svg
|
|
84
|
+
pdf/logo-light: assets/logo-white.svg
|
|
85
|
+
pdf/footer-text: Generated with Archsight
|
|
86
|
+
pdf/classification: Internal
|
|
87
|
+
pdf/font-body: Open Sans
|
|
88
|
+
pdf/font-dir: assets/fonts
|
|
89
|
+
spec: {}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Annotation | Meaning |
|
|
93
|
+
|------------|---------|
|
|
94
|
+
| `config/type` | `pdf` for these settings |
|
|
95
|
+
| `pdf/color-primary`, `-accent`, `-text`, `-muted`, `-ok`, `-warn`, `-danger` | Hex colours (`#rrggbb`) |
|
|
96
|
+
| `pdf/font-body`, `-heading`, `-mono` | Font families, tried before the built-in fallbacks |
|
|
97
|
+
| `pdf/font-dir` | Directory (relative to the resources directory) with font files (ttf, otf) that ship with the model |
|
|
98
|
+
| `pdf/logo`, `pdf/logo-light` | svg or png, relative to the resources directory. `logo` is shown on white (page header), `logo-light` on the cover band |
|
|
99
|
+
| `pdf/footer-text`, `pdf/classification` | Texts in the footer of every page |
|
|
100
|
+
| `pdf/header-meta` | Parts of the header line: `id, version, date, owner` |
|
|
101
|
+
| `pdf/lang` | Language of the fixed texts: `en` or `de` |
|
|
102
|
+
| `pdf/paper`, `pdf/margin` | `a4`, `a5` or `us-letter`; horizontal margin such as `16mm` |
|
|
103
|
+
|
|
104
|
+
Values are checked when they are loaded (and by `archsight lint`): something that is not a colour, a font name or a length is
|
|
105
|
+
ignored and the default is used.
|
|
106
|
+
|
|
107
|
+
## Seeing the look
|
|
108
|
+
|
|
109
|
+
The page of a Config of type `pdf` shows its settings as they look (the colours, the fonts, the logo on white and on
|
|
110
|
+
the cover band, the page settings) and, beside them, a **preview**: a fixed sample document (a cover page and a page with
|
|
111
|
+
headings, a list, a quote, a table with status badges and code) typeset by the server with exactly these settings, one
|
|
112
|
+
tab for the cover and one for the page. It shows the saved Config; after an edit the page typesets it again.
|
|
113
|
+
The same sample is `GET /api/v1/kinds/Config/instances/{name}/preview` (`?part=cover` or `?part=page` for one page).
|
|
114
|
+
|
|
115
|
+
## Settings
|
|
116
|
+
|
|
117
|
+
`pdf.typst` (`ARCHSIGHT_PDF_TYPST`) is the path of the `typst` command if it is not on the `PATH`, `pdf.dot` (`ARCHSIGHT_PDF_DOT`) the same for `dot`, `pdf.timeout`
|
|
118
|
+
(`ARCHSIGHT_PDF_TIMEOUT`) the seconds a document may take (default 60), `pdf.remote_images` (`ARCHSIGHT_PDF_REMOTE_IMAGES`) `false` to never fetch images by URL, `pdf.max_analyses` (`ARCHSIGHT_PDF_MAX_ANALYSES`) the number of analyses one document may run (default 5). See [Configuration](/doc/configuration).
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../markdown"
|
|
4
|
+
require_relative "../pdf/typst"
|
|
5
|
+
require_relative "../pdf/markdown"
|
|
4
6
|
require_relative "../diagram"
|
|
5
7
|
|
|
6
8
|
module Archsight
|
|
@@ -10,20 +12,18 @@ module Archsight
|
|
|
10
12
|
# All text is emitted as Typst string literals (`#"..."`) and structure as
|
|
11
13
|
# function calls (`#strong[...]`), so no content can be mistaken for markup.
|
|
12
14
|
class TypstRenderer
|
|
15
|
+
include Archsight::Pdf::Typst
|
|
16
|
+
|
|
13
17
|
ROW_LIMIT = 10
|
|
14
18
|
HEADING_OFFSET = 2 # section level 0 becomes a level-2 heading, the analysis name is level 1
|
|
15
|
-
LANGUAGE = /\A[A-Za-z0-9+#-]+\z/
|
|
16
19
|
MESSAGE_FILL = { error: "rgb(\"#fde8e8\")", warning: "rgb(\"#fff3cc\")", info: "rgb(\"#ddeeff\")" }.freeze
|
|
17
|
-
SYMBOLS = {
|
|
18
|
-
ndash: "–", mdash: "—", hellip: "…", laquo: "«", raquo: "»", laquo_space: "« ", raquo_space: " »",
|
|
19
|
-
lsquo: "‘", rsquo: "’", ldquo: "“", rdquo: "”"
|
|
20
|
-
}.freeze
|
|
21
20
|
|
|
22
21
|
PREAMBLE = <<~TYPST
|
|
23
22
|
#set document(title: "Archsight analysis")
|
|
24
23
|
#set page(paper: "a4", margin: 2cm, numbering: "1")
|
|
25
24
|
#set text(size: 10pt)
|
|
26
25
|
#show raw.where(block: true): it => block(fill: luma(245), inset: 8pt, radius: 3pt, width: 100%, it)
|
|
26
|
+
#set table(stroke: (x, y) => if y == 0 { (bottom: 0.7pt + luma(150)) } else { (bottom: 0.4pt + luma(220)) }, inset: (x, y) => (left: if x == 0 { 0pt } else { 6pt }, right: 6pt, y: 6pt))
|
|
27
27
|
TYPST
|
|
28
28
|
|
|
29
29
|
# @param results [Array<Archsight::Analysis::Result>]
|
|
@@ -31,10 +31,12 @@ module Archsight
|
|
|
31
31
|
# @param template [String, nil] Typst file to import instead of the built-in layout. It must define
|
|
32
32
|
# `archsight-report` (a show rule for the document) and `analysis-result` (one call per analysis,
|
|
33
33
|
# with the data sections as a dictionary and the other sections as body).
|
|
34
|
-
|
|
34
|
+
# @param full [Boolean] show every table row and list item, without the error backtrace of `verbose`
|
|
35
|
+
def initialize(results, verbose: false, template: nil, full: false)
|
|
35
36
|
@results = results
|
|
36
37
|
@verbose = verbose
|
|
37
38
|
@template = template
|
|
39
|
+
@full = full
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
# @return [String] Typst source
|
|
@@ -44,6 +46,17 @@ module Archsight
|
|
|
44
46
|
[PREAMBLE, title, summary, *@results.map { |result| render_result(result) }].join("\n")
|
|
45
47
|
end
|
|
46
48
|
|
|
49
|
+
# One section of a result as Typst markup (nil for data that only templates use)
|
|
50
|
+
# @param heading_offset [Integer] added to the level of a heading section
|
|
51
|
+
def section(section, heading_offset: HEADING_OFFSET)
|
|
52
|
+
render_section(section, heading_offset)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The error of a failed result as Typst markup
|
|
56
|
+
def error(result)
|
|
57
|
+
error_block(result)
|
|
58
|
+
end
|
|
59
|
+
|
|
47
60
|
private
|
|
48
61
|
|
|
49
62
|
def title
|
|
@@ -120,9 +133,9 @@ module Archsight
|
|
|
120
133
|
lines.join("\n\n")
|
|
121
134
|
end
|
|
122
135
|
|
|
123
|
-
def render_section(section)
|
|
136
|
+
def render_section(section, heading_offset = HEADING_OFFSET)
|
|
124
137
|
case section[:type]
|
|
125
|
-
when :heading then "#heading(level: #{[section[:level] +
|
|
138
|
+
when :heading then "#heading(level: #{[section[:level] + heading_offset, 6].min})[#{str(section[:text])}]"
|
|
126
139
|
when :text then markdown(section[:content])
|
|
127
140
|
when :table then table(section[:headers], section[:rows])
|
|
128
141
|
when :list then list(section[:items])
|
|
@@ -139,15 +152,14 @@ module Archsight
|
|
|
139
152
|
|
|
140
153
|
# A template produces a document to hand out, so it gets every row
|
|
141
154
|
def full?
|
|
142
|
-
@verbose || !@template.nil?
|
|
155
|
+
@verbose || @full || !@template.nil?
|
|
143
156
|
end
|
|
144
157
|
|
|
145
158
|
def table(headers, rows)
|
|
146
159
|
shown = full? ? rows : rows.first(ROW_LIMIT)
|
|
147
|
-
head = headers.map { |header| "[#
|
|
160
|
+
head = headers.map { |header| "[#text(weight: \"semibold\")[#{str(header)}]]" }
|
|
148
161
|
cells = shown.flat_map { |row| headers.each_index.map { |i| "[#{str(row[i])}]" } }
|
|
149
|
-
out = "#table(columns: #{headers.size},
|
|
150
|
-
"table.header(#{head.join(", ")}), #{cells.join(", ")})"
|
|
162
|
+
out = "#table(columns: #{headers.size}, table.header(#{head.join(", ")}), #{cells.join(", ")})"
|
|
151
163
|
out += "\n\n#emph[#{str("...and #{rows.size - ROW_LIMIT} more rows")}]" if shown.size < rows.size
|
|
152
164
|
out
|
|
153
165
|
end
|
|
@@ -172,73 +184,8 @@ module Archsight
|
|
|
172
184
|
[message_block(:error, "Diagram could not be rendered: #{e.message}"), raw(source, "asd")].join("\n\n")
|
|
173
185
|
end
|
|
174
186
|
|
|
175
|
-
def raw(source, lang)
|
|
176
|
-
lang_arg = lang.to_s.match?(LANGUAGE) ? ", lang: #{lit(lang)}" : ""
|
|
177
|
-
"#raw(#{lit(source.to_s.chomp)}, block: true#{lang_arg})"
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# A Typst string literal, for use as a function argument
|
|
181
|
-
def lit(text)
|
|
182
|
-
escaped = text.to_s.delete("\r").gsub(/[\\"]/) { "\\#{Regexp.last_match(0)}" }.gsub("\n", "\\n")
|
|
183
|
-
"\"#{escaped}\""
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# A Typst string literal as markup, for use inside content
|
|
187
|
-
def str(text)
|
|
188
|
-
"##{lit(text)}"
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# ---- markdown -> Typst
|
|
192
|
-
|
|
193
187
|
def markdown(source, inline: false)
|
|
194
|
-
|
|
195
|
-
out = blocks(root.children)
|
|
196
|
-
inline ? out.gsub(/\n\n+/, " #linebreak() ") : out
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def blocks(elements)
|
|
200
|
-
elements.filter_map { |node| block(node) }.join("\n\n")
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def block(node)
|
|
204
|
-
case node.type
|
|
205
|
-
when :p then inlines(node.children)
|
|
206
|
-
when :header then "#heading(level: #{[node.options[:level] + 1, 6].min})[#{inlines(node.children)}]"
|
|
207
|
-
when :codeblock then raw(node.value, node.options[:lang])
|
|
208
|
-
when :blockquote then "#quote(block: true)[#{blocks(node.children)}]"
|
|
209
|
-
when :ul then "#list(#{node.children.map { |li| "[#{blocks(li.children)}]" }.join(", ")})"
|
|
210
|
-
when :ol then "#enum(#{node.children.map { |li| "[#{blocks(li.children)}]" }.join(", ")})"
|
|
211
|
-
when :hr then "#line(length: 100%)"
|
|
212
|
-
when :table then markdown_table(node)
|
|
213
|
-
when :blank, :html_element, :xml_comment then nil
|
|
214
|
-
else inlines(node.children) unless node.children.empty?
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def markdown_table(node)
|
|
219
|
-
rows = node.children.flat_map(&:children)
|
|
220
|
-
cells = rows.map { |tr| tr.children.map { |td| "[#{inlines(td.children)}]" } }
|
|
221
|
-
width = cells.map(&:size).max || 1
|
|
222
|
-
"#table(columns: #{width}, inset: 5pt, stroke: 0.5pt + luma(200), #{cells.flatten.join(", ")})"
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def inlines(elements)
|
|
226
|
-
elements.map { |node| inline(node) }.join
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def inline(node)
|
|
230
|
-
case node.type
|
|
231
|
-
when :text then str(node.value)
|
|
232
|
-
when :em then "#emph[#{inlines(node.children)}]"
|
|
233
|
-
when :strong then "#strong[#{inlines(node.children)}]"
|
|
234
|
-
when :codespan then "#raw(#{lit(node.value)})"
|
|
235
|
-
when :a then "#link(#{lit(node.attr["href"])})[#{inlines(node.children)}]"
|
|
236
|
-
when :br then "#linebreak()"
|
|
237
|
-
when :entity then str(node.value.char)
|
|
238
|
-
when :smart_quote, :typographic_sym then str(SYMBOLS.fetch(node.value, node.value.to_s))
|
|
239
|
-
when :html_element then inlines(node.children)
|
|
240
|
-
else inlines(node.children)
|
|
241
|
-
end
|
|
188
|
+
Archsight::Pdf::Markdown.new(heading_offset: 1, diagram: method(:diagram)).render(source, inline: inline)
|
|
242
189
|
end
|
|
243
190
|
end
|
|
244
191
|
end
|
data/lib/archsight/cli.rb
CHANGED
|
@@ -279,6 +279,7 @@ module Archsight
|
|
|
279
279
|
option :theme, type: :string, desc: "Spacing/font-size theme (default: the file's own `theme` statement)"
|
|
280
280
|
option :legend, type: :string, desc: "Legend placement (default: the file's own `legend` statement, else auto)"
|
|
281
281
|
option :profile, aliases: "-p", type: :boolean, desc: "Print stage timing stats after each render"
|
|
282
|
+
option :resources, aliases: "-r", type: :string, desc: "Directory that `include \"file://...\"` paths are relative to (default: the input's directory)"
|
|
282
283
|
option :format, type: :boolean, desc: "Re-indent the inputs in place instead of rendering them (-o: write a single input there)"
|
|
283
284
|
option :check, type: :boolean, desc: "With --format: only list the inputs that are not formatted, exit 1 if any"
|
|
284
285
|
def diagram(*inputs)
|
|
@@ -295,7 +296,7 @@ module Archsight
|
|
|
295
296
|
|
|
296
297
|
status = diagram::CLI.new.run_with(
|
|
297
298
|
inputs: inputs, output: options[:output], watch: options[:watch], profile: options[:profile],
|
|
298
|
-
format: options[:format], check: options[:check],
|
|
299
|
+
format: options[:format], check: options[:check], resources: options[:resources],
|
|
299
300
|
style: options[:style], theme: options[:theme], legend: options[:legend],
|
|
300
301
|
relation_filter: if relation.nil?
|
|
301
302
|
diagram::Relations::DEFAULT_FILTER
|
|
@@ -355,6 +356,43 @@ module Archsight
|
|
|
355
356
|
exit 1
|
|
356
357
|
end
|
|
357
358
|
|
|
359
|
+
desc "pdf TARGET", "Export a page or resource as PDF (needs typst)"
|
|
360
|
+
long_desc <<~DESC
|
|
361
|
+
Typesets a page (TARGET is its name) or any other resource (TARGET is Kind/Name) as a PDF with Typst, the same
|
|
362
|
+
document the PDF button of the web UI downloads. The look comes from the Config of type pdf named default (or --template),
|
|
363
|
+
the tags pdf:cover, pdf:toc, pdf:landscape, pdf:draft and pdf:template:<name> steer the layout. The typst command
|
|
364
|
+
is taken from the PATH, or from ARCHSIGHT_PDF_TYPST / `pdf.typst` in ~/.config/archsight/archsight.yaml.
|
|
365
|
+
See the PDF Export page of the documentation.
|
|
366
|
+
DESC
|
|
367
|
+
option :output, aliases: "-o", type: :string, desc: "Output file (default: <name>.pdf in the current directory)"
|
|
368
|
+
option :template, type: :string, desc: "Name of the pdf Config to use (default: the tag or `default`)"
|
|
369
|
+
option :draft, type: :boolean, default: false, desc: "Add the draft watermark"
|
|
370
|
+
def pdf(target)
|
|
371
|
+
configure_resources
|
|
372
|
+
require "archsight/database"
|
|
373
|
+
require "archsight/pdf"
|
|
374
|
+
|
|
375
|
+
db = Archsight::Database.new(Archsight.resources_dir, compute_annotations: false)
|
|
376
|
+
begin
|
|
377
|
+
db.reload!
|
|
378
|
+
rescue Archsight::ResourceError => e
|
|
379
|
+
display_error_with_context(e.to_s)
|
|
380
|
+
exit 1
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
kind, name = target.include?("/") ? target.split("/", 2) : ["Page", target]
|
|
384
|
+
resource = Archsight::Resources[kind] && (db.instance_by_kind(kind, name) || db.instance_by_alias(kind, name))
|
|
385
|
+
abort "Error: #{kind} '#{name}' not found" unless resource
|
|
386
|
+
|
|
387
|
+
document = Archsight::Pdf.document(db, resource, template: options[:template], draft: options[:draft])
|
|
388
|
+
file = options[:output] || document.filename
|
|
389
|
+
File.binwrite(file, Archsight::Pdf.compile(document))
|
|
390
|
+
puts "Wrote #{file}"
|
|
391
|
+
rescue Archsight::Pdf::Error => e
|
|
392
|
+
warn "Error: #{e.message}"
|
|
393
|
+
exit 1
|
|
394
|
+
end
|
|
395
|
+
|
|
358
396
|
desc "module SUBCOMMAND", "Module analysis commands (e.g. module graph PATH)"
|
|
359
397
|
subcommand "module", ModuleCLI
|
|
360
398
|
|
data/lib/archsight/database.rb
CHANGED
|
@@ -109,11 +109,12 @@ module Archsight
|
|
|
109
109
|
matches.first if matches.one?
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
#
|
|
112
|
+
# Annotation values across all instances of a kind with the number of instances using each,
|
|
113
|
+
# most used first (ties by value): [[value, count], ...]
|
|
113
114
|
def annotation_values(kind, annotation)
|
|
114
115
|
instances = instances_by_kind(kind).values
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
counts = instances.flat_map { |inst| Array(annotation.value_for(inst)).compact.uniq }.tally
|
|
117
|
+
counts.sort_by { |value, count| [-count, value.to_s] }
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
# Get filterable annotations with their values for a kind (excludes empty)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require_relative "."
|
|
5
5
|
require_relative "formatter"
|
|
6
|
+
require_relative "../helpers/diagram_includes"
|
|
6
7
|
|
|
7
8
|
module Archsight
|
|
8
9
|
module Diagram
|
|
@@ -79,6 +80,8 @@ module Archsight
|
|
|
79
80
|
opts.on("-p", "--profile", "Print stage timing stats after each render") do
|
|
80
81
|
options[:profile] = true
|
|
81
82
|
end
|
|
83
|
+
opts.on("--resources=DIR", "Directory that include \"file://...\" paths are relative to " \
|
|
84
|
+
"(default: the input file's directory)") { |v| options[:resources] = v }
|
|
82
85
|
opts.on("--format", "Re-indent the inputs in place instead of rendering them (-o: write a single input there)") do
|
|
83
86
|
options[:format] = true
|
|
84
87
|
end
|
|
@@ -155,7 +158,9 @@ module Archsight
|
|
|
155
158
|
total_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) if profile
|
|
156
159
|
|
|
157
160
|
svg = Archsight::Diagram.render(File.read(input), relation_filter: options[:relation_filter], profile: stats,
|
|
158
|
-
style: options[:style], theme: options[:theme], legend: options[:legend]
|
|
161
|
+
style: options[:style], theme: options[:theme], legend: options[:legend],
|
|
162
|
+
includes: includes_for(input, options), warnings: warnings = [])
|
|
163
|
+
warnings.each { |message| warn "archsight-diagram: #{input}: #{message}" }
|
|
159
164
|
|
|
160
165
|
write_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) if profile
|
|
161
166
|
File.write(output, svg)
|
|
@@ -172,6 +177,11 @@ module Archsight
|
|
|
172
177
|
1
|
|
173
178
|
end
|
|
174
179
|
|
|
180
|
+
# Standalone, `include` reads `file://` paths only (there is no model for `archsight://`)
|
|
181
|
+
def includes_for(input, options)
|
|
182
|
+
Helpers::DiagramIncludes.new(nil, resources_dir: options[:resources] || File.dirname(File.expand_path(input)))
|
|
183
|
+
end
|
|
184
|
+
|
|
175
185
|
def print_stats(stats, total_start)
|
|
176
186
|
total = Process.clock_gettime(Process::CLOCK_MONOTONIC) - total_start
|
|
177
187
|
parts = stats.map { |key, seconds| "#{key}=#{format("%.1fms", seconds * 1000)}" }
|
|
@@ -27,13 +27,14 @@ module Archsight
|
|
|
27
27
|
NODE = %w[label tint link resource extend].freeze
|
|
28
28
|
LEAF_KINDS = %i[component application api database queue actor file].freeze
|
|
29
29
|
LEAF = (NODE + %w[shape]).freeze
|
|
30
|
-
CONTAINER = (NODE + %w[gap ranks columns]).freeze
|
|
30
|
+
CONTAINER = (NODE + %w[gap ranks columns anchor]).freeze
|
|
31
31
|
EDGE = %w[label style relation tint].freeze
|
|
32
32
|
DATAFLOW = %w[label color].freeze
|
|
33
33
|
|
|
34
34
|
EDGE_STYLES = %w[straight orthogonal].freeze
|
|
35
35
|
LEAF_EXTEND = %w[true false].freeze
|
|
36
36
|
CONTAINER_EXTEND = %w[true false height].freeze
|
|
37
|
+
ANCHORS = %w[top bottom left right center].freeze
|
|
37
38
|
|
|
38
39
|
# `40` (pixels), `150%` (of the default), `+20%`/`-50%` (relative);
|
|
39
40
|
# see `GapSpec`.
|
|
@@ -116,6 +117,7 @@ module Archsight
|
|
|
116
117
|
when "tint" then check_value!(key, value, Tints.names, where, line)
|
|
117
118
|
when "ranks" then check_value!(key, value, ranks_modes, where, line)
|
|
118
119
|
when "extend" then check_value!(key, value, leaf?(block) ? LEAF_EXTEND : CONTAINER_EXTEND, where, line)
|
|
120
|
+
when "anchor" then check_value!(key, value, ANCHORS, where, line)
|
|
119
121
|
when "gap" then check_format!(key, value, GAP_FORMAT, "a pixel count (40), a percentage (150%) or a signed one (+20%, -50%)", where, line)
|
|
120
122
|
when "link" then check_link!(value, where, line)
|
|
121
123
|
when "resource" then check_resource!(value, block, where, line)
|
|
@@ -130,11 +130,28 @@ module Archsight
|
|
|
130
130
|
# `extend "height"` on a direct child of a `layer` grows that
|
|
131
131
|
# child's box to its tallest sibling's height, leaving its own
|
|
132
132
|
# content top-aligned where it already sits. Never inherited --
|
|
133
|
-
# only the node carrying the attr is affected.
|
|
133
|
+
# only the node carrying the attr is affected. In a `stack` inside such a
|
|
134
|
+
# grown box it absorbs the extra height instead (see
|
|
135
|
+
# `Expander#grow_height`).
|
|
134
136
|
def extend_height?
|
|
135
137
|
attrs["extend"] == "height"
|
|
136
138
|
end
|
|
137
139
|
|
|
140
|
+
# Where this container's content sits when its box grows by `extra`
|
|
141
|
+
# along `axis`: the `anchor` attr (`top`/`bottom` for height,
|
|
142
|
+
# `left`/`right` for width, `center`), else `default`. Returns how
|
|
143
|
+
# far to shift the children (0 = leading edge, `extra` = trailing).
|
|
144
|
+
# Never inherited; a value for the other axis is ignored.
|
|
145
|
+
def anchor_shift(axis, extra, default:)
|
|
146
|
+
value = attrs["anchor"]
|
|
147
|
+
value = default.to_s unless (axis == Axis::HEIGHT ? %w[top bottom center] : %w[left right center]).include?(value)
|
|
148
|
+
case value
|
|
149
|
+
when "top", "left" then 0.0
|
|
150
|
+
when "bottom", "right" then extra.to_f
|
|
151
|
+
else extra / 2.0
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
138
155
|
def link
|
|
139
156
|
attrs["link"]
|
|
140
157
|
end
|