@r0kshan/cairn 1.0.0-RC13 → 1.0.0-RC15

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.
package/bin/cairn.mjs CHANGED
@@ -1,4 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ /*!
3
+ * cairn — Apache-2.0 — https://github.com/R0kshan/cairn
4
+ * Copyright 2026 Røkshan.
5
+ *
6
+ * This artifact contains third-party code and artwork:
7
+ *
8
+ * elkjs 0.12.0 — (c) 2017 Kiel University and others.
9
+ * Dual-licensed EPL-2.0 OR GPL-3.0-or-later; cairn elects EPL-2.0.
10
+ * Full text: licenses/elkjs-EPL-2.0.md — https://www.eclipse.org/legal/epl-2.0
11
+ *
12
+ * Simple Icons 16.29.0 artwork — CC0-1.0 project-wide.
13
+ * Six of the vendored icons carry their own terms instead — MIT,
14
+ * BSD-3-Clause, CC-BY-4.0 or Apache-2.0 — and are attributed individually.
15
+ * Full texts: licenses/
16
+ *
17
+ * Per-icon attribution and the full licence texts ship with this artifact:
18
+ * the package root (npm), the app directory (Scoop), or
19
+ * share/doc/cairn (Homebrew, install.sh). Also at
20
+ * https://github.com/R0kshan/cairn/blob/main/THIRD-PARTY-NOTICES.md
21
+ */
2
22
  var __create = Object.create;
3
23
  var __defProp = Object.defineProperty;
4
24
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -251,13 +271,13 @@ var require_elk_bundled = __commonJS({
251
271
  this.id = id + 1;
252
272
  msg.id = id;
253
273
  var self2 = this;
254
- return new Promise(function(resolve, reject) {
274
+ return new Promise(function(resolve2, reject) {
255
275
  self2.resolvers[id] = function(err, res) {
256
276
  if (err) {
257
277
  self2.convertGwtStyleError(err);
258
278
  reject(err);
259
279
  } else {
260
- resolve(res);
280
+ resolve2(res);
261
281
  }
262
282
  };
263
283
  self2.worker.postMessage(msg);
@@ -92320,11 +92340,12 @@ var init_lexer = __esm({
92320
92340
  });
92321
92341
 
92322
92342
  // src/models/ast.ts
92323
- var ATTACH_SIDES, defaultDiagramStyle, explanations;
92343
+ var ATTACH_SIDES, ATTACH_ROLES, defaultDiagramStyle, explanations;
92324
92344
  var init_ast = __esm({
92325
92345
  "src/models/ast.ts"() {
92326
92346
  "use strict";
92327
92347
  ATTACH_SIDES = ["left", "right", "top", "bottom"];
92348
+ ATTACH_ROLES = ["producer", "consumer"];
92328
92349
  defaultDiagramStyle = () => ({
92329
92350
  crossingHops: true,
92330
92351
  compact: false,
@@ -92343,7 +92364,11 @@ var init_ast = __esm({
92343
92364
  });
92344
92365
  explanations = {
92345
92366
  E0101: "Syntax error: the file does not follow the DSL grammar.",
92367
+ E0105: '`logo:` takes either a built-in name (`logo: react`) or a quoted path to a file in the workspace (`logo: "./logos/acme.svg"`). A URL is refused on purpose: a diagram that fetches its logos stops being self-contained, leaks the reader\'s IP to whoever hosts them, and rots silently when the address dies \u2014 so cairn inlines a file instead of linking one.',
92346
92368
  E0106: "`order:` takes a whole number \u2265 0. On a top-level element it sets the reading order the disposition defines \u2014 left to right for `wide`/`slide`, top to bottom for `tall`/`page` \u2014 by banding the element inside its own layout partition, so it never crosses into another partition. Inside a container the axis flips: a child's layer belongs to the flows and elk honors no constraint that would change it, so there `order:` sorts the siblings that share a layer. Values need not be contiguous.",
92369
+ E0107: 'Unknown built-in logo. The built-in set is deliberately small \u2014 it covers common stack technologies, not every brand. Anything outside it is still reachable as a workspace file: `logo: "./logos/name.svg"`.',
92370
+ E0108: "This element kind does not carry a logo. A logo marks what a piece of software is built with, so the kinds that accept one are the kinds that run code; actors are people and containers are groupings, so neither takes one.",
92371
+ W0580: "A `logo:` file could not be inlined \u2014 it is missing, too large, or not one of the supported types. Only a warning: the diagram is still valid and renders without the mark, because a decoration that failed to load is no reason to fail a build.",
92347
92372
  E0201: "Unknown element kind for the active view. Each view defines its own kinds (e.g. logical: actor-group, actor, system, layer, block, external).",
92348
92373
  E0202: "Duplicate identifier. IDs are flat and unique per file (decision D1): every element needs a distinct ID so flows can reference it unambiguously.",
92349
92374
  E0203: "The logical view forbids unlabelled arrows: every flow must name the data, command or event it carries. A logical diagram tells what circulates, not just who talks to whom.",
@@ -92360,42 +92385,53 @@ var init_ast = __esm({
92360
92385
  E0215: "A deployed application (app-instance) must sit on a server or in a zone. It shows WHERE an application runs, without its internal detail (C4 deployment convention).",
92361
92386
  E0216: "Network zones belong to a site (or nest inside a larger zone): DMZ and LAN only mean something relative to a perimeter.",
92362
92387
  W0540: 'C4 container-diagram practice: inter-process relationships should be labelled with their technology/protocol ("the how, not just the what"). Human/actor interactions are exempt. Add `(API_REST, JSON)` after the label, or ignore if the diagram is intentionally functional-only.',
92363
- E0223: "Unknown attachment side. A flow endpoint may name the side it attaches to \u2014 `APP.right -> DB.left` \u2014 using the diagram as it is read: `left`, `right`, `top`, `bottom`. The side is a hint, not a guarantee: a side the layout cannot reach is dropped with a W0570 warning rather than forced.",
92388
+ E0223: "Unknown endpoint suffix. A flow endpoint may name the side it attaches to \u2014 `APP.right -> DB.left` \u2014 using the diagram as it is read: `left`, `right`, `top`, `bottom`. It may instead name its role towards a queue at the other end: `producer` or `consumer`. A declared side is a hint, not a guarantee: one the layout cannot reach is dropped with a W0570 warning rather than forced.",
92389
+ E0224: "An endpoint role says what this element does with the *queue* at the other end of the flow (`CAPTURE.producer -> EVENTS`), so the other end has to be one. Point the flow at a queue, or drop the role \u2014 between two ordinary elements the arrow already says everything a role would.",
92390
+ E0225: "A flow's queue endpoint carries both a declared side and a role at the other end, and they are two answers to one question: `producer` already means the queue's left cap and `consumer` its right. Keep the role and drop the side, or keep the side and drop the role.",
92364
92391
  E0240: "The infrastructure view requires every flow to carry its protocol (and port if relevant): the flow matrix is the primary output of this view. Add `(HTTPS/443)` after the label.",
92365
92392
  E0221: 'Unknown business-object reference: a flow carries `[AN_ID]` that no `business-object` declaration defines. Declare it once (`business-object ID "Name" "description"`) so the registry and the chips stay consistent.',
92366
- E0222: "Business objects are a logical-view concept (what data circulates between functional blocks). They are not part of the application, infrastructure, or security views \u2014 model the exchange with the flow label and technical tail instead. Remove the `business-object` declaration and its `[refs]`, or switch the diagram to `logical`.",
92393
+ E0222: "Business objects are a logical-view concept (what data circulates between functional blocks). They are not part of the application or infrastructure views \u2014 model the exchange with the flow label and technical tail instead. Remove the `business-object` declaration and its `[refs]`, or switch the diagram to `logical`.",
92367
92394
  W0530: "Completeness check: a declared business object is never carried by any flow \u2014 either connect it to the flows that transport it, or remove it from this view.",
92368
- E0217: "A sensitive asset must sit inside a trust zone: its protection level is defined by the zone that contains it.",
92369
- E0218: "A security node (firewall, WAF, bastion, reverse proxy) lives inside a trust zone \u2014 typically the exposed zone whose traffic it filters.",
92370
- E0250: 'The security view requires each trust zone to declare a sensitivity level in parentheses after the label: `trust-zone DMZ "DMZ" (public)`. Levels, least \u2192 most trusted: public, internal, restricted, secret. The level drives the zone color and the trust-boundary crossing checks.',
92371
92395
  W0570: "A declared attachment side (`APP.right -> DB.left`) did not survive layout: the flow ended up on another side of the element. Pins are honored where the layout can reach them and dropped where it cannot, rather than forced into an unreadable route. Move the element instead (`order:`), pin the other endpoint, or drop the pin.",
92372
- W0571: "An endpoint reads `ID.side`, but `ID.side` is itself a declared element, and a declared id always wins \u2014 so the flow attaches to that element and no side is pinned. Rename the element if you meant the side.",
92373
- W0560: "Security check: this flow enters a more-trusted zone from a less-trusted one without passing through a security-node (firewall/WAF/bastion). Route it through a filtering point, or confirm the direct path is deliberate. This is the diagram equivalent of a missing firewall rule review.",
92374
- W0561: "Security check: an inter-zone flow does not state its encryption/protocol. Cross-zone traffic should declare how it is protected: add `(TLS1.3)` or the relevant protocol after the label."
92396
+ W0571: "An endpoint reads `ID.side`, but `ID.side` is itself a declared element, and a declared id always wins \u2014 so the flow attaches to that element and no side is pinned. Rename the element if you meant the side."
92375
92397
  };
92376
92398
  }
92377
92399
  });
92378
92400
 
92379
92401
  // src/themes.ts
92402
+ function isDarkTheme(name) {
92403
+ return name === "classic-dark" || THEME_SPECS[name]?.dark === true;
92404
+ }
92405
+ function registerTheme(name, spec) {
92406
+ if (RESERVED_THEME_NAMES.includes(name)) {
92407
+ throw new Error(`\`${name}\` is a reserved theme name \u2014 rename the theme file`);
92408
+ }
92409
+ THEME_SPECS[name] = spec;
92410
+ themes[name] = buildTheme(spec);
92411
+ if (!themeNames.includes(name)) themeNames.push(name);
92412
+ }
92380
92413
  function themeFor(name, view) {
92381
- if (name === "classic")
92382
- return {
92383
- palette: lightPalette,
92384
- kinds: view.defaults,
92385
- levels: view.levelDefaults ?? {}
92386
- };
92387
- if (name === "classic-dark")
92388
- return {
92389
- palette: darkPalette,
92390
- kinds: view.defaultsDark,
92391
- levels: view.levelDefaultsDark ?? {}
92392
- };
92393
- const theme = themes[name] ?? themes.light;
92414
+ if (name === "classic") return { palette: lightPalette, kinds: view.defaults };
92415
+ if (name === "classic-dark") return { palette: darkPalette, kinds: view.defaultsDark };
92416
+ return applyTheme(themes[name] ?? themes.light, view);
92417
+ }
92418
+ function themeFromSpec(spec, view) {
92419
+ return applyTheme(buildTheme(spec), view);
92420
+ }
92421
+ function paletteFor(name) {
92422
+ if (name === "classic") return lightPalette;
92423
+ if (name === "classic-dark") return darkPalette;
92424
+ return themes[name]?.palette ?? lightPalette;
92425
+ }
92426
+ function paletteFromSpec(spec) {
92427
+ return buildTheme(spec).palette;
92428
+ }
92429
+ function applyTheme(theme, view) {
92394
92430
  const kinds = {};
92395
- for (const kind of view.kinds) kinds[kind] = theme.roles[roleForKind(kind, view.name)] ?? {};
92396
- return { palette: theme.palette, kinds, levels: theme.levels };
92431
+ for (const kind of view.kinds) kinds[kind] = theme.roles[roleForKind(kind)] ?? {};
92432
+ return { palette: theme.palette, kinds };
92397
92433
  }
92398
- var lightPalette, darkPalette, palettes, flowPalette, containerStyle, leafStyle, KIND_ROLE_MAP, roleForKind, buildTheme, themes, themeNames;
92434
+ var lightPalette, darkPalette, flowPalette, containerStyle, leafStyle, KIND_ROLE_MAP, roleForKind, buildTheme, THEME_SPECS, themes, themeNames, RESERVED_THEME_NAMES;
92399
92435
  var init_themes = __esm({
92400
92436
  "src/themes.ts"() {
92401
92437
  "use strict";
@@ -92447,10 +92483,6 @@ var init_themes = __esm({
92447
92483
  chipStroke: "#b08d2a",
92448
92484
  chipText: "#e0c068"
92449
92485
  };
92450
- palettes = {
92451
- light: lightPalette,
92452
- dark: darkPalette
92453
- };
92454
92486
  flowPalette = {
92455
92487
  light: [
92456
92488
  "#1f77b4",
@@ -92497,16 +92529,15 @@ var init_themes = __esm({
92497
92529
  datastore: "datastore",
92498
92530
  queue: "datastore",
92499
92531
  gateway: "authGateway",
92532
+ firewall: "firewall",
92500
92533
  auth: "auth",
92501
92534
  idp: "identityProvider",
92502
92535
  site: "site",
92503
92536
  "network-zone": "networkZone",
92504
92537
  server: "server",
92505
- "app-instance": "appInstance",
92506
- "security-node": "securityNode",
92507
- asset: "leaf"
92538
+ "app-instance": "appInstance"
92508
92539
  };
92509
- roleForKind = (kind, viewName) => viewName === "security" && kind === "external" ? "untrusted" : KIND_ROLE_MAP[kind] ?? "leaf";
92540
+ roleForKind = (kind) => KIND_ROLE_MAP[kind] ?? "leaf";
92510
92541
  buildTheme = (spec) => {
92511
92542
  const paletteSpec = spec.pal, accentColors = spec.accentColors;
92512
92543
  const palette = {
@@ -92542,28 +92573,25 @@ var init_themes = __esm({
92542
92573
  application: containerStyle(accentColors.appF, accentColors.app),
92543
92574
  layer: containerStyle(accentColors.goldF, accentColors.gold, false, 1),
92544
92575
  external: containerStyle(accentColors.violetF, accentColors.violet, true),
92545
- untrusted: containerStyle(accentColors.redF, accentColors.red, true, 1.3),
92546
92576
  leaf: leafStyle(accentColors.leafF, accentColors.leafS),
92547
92577
  datastore: leafStyle(accentColors.purpleF, accentColors.purple),
92548
92578
  site: containerStyle(accentColors.siteF, accentColors.siteS, false, 1.4),
92549
92579
  networkZone: containerStyle(accentColors.greenF, accentColors.green, true),
92550
92580
  server: containerStyle(accentColors.serverF, accentColors.serverS, false, 1.5),
92551
92581
  appInstance: leafStyle(accentColors.aiF, accentColors.aiS, 1.2),
92552
- securityNode: leafStyle(accentColors.nodeF, accentColors.node, 1.6),
92582
+ // Accent keys predate the roles that read them: `auth`/`authF` is the
92583
+ // gateway's orange, `authn`/`authnF` the auth middleware's blue. The two
92584
+ // used to share the `node` orange, which is what made a gateway and an
92585
+ // auth middleware indistinguishable in a dense infrastructure view.
92553
92586
  authGateway: leafStyle(accentColors.authF, accentColors.auth, 1.4),
92554
- auth: leafStyle(accentColors.nodeF, accentColors.node, 1.5),
92587
+ firewall: leafStyle(accentColors.fwF, accentColors.fw, 1.6),
92588
+ auth: leafStyle(accentColors.authnF, accentColors.authn, 1.5),
92555
92589
  identityProvider: leafStyle(accentColors.idpF, accentColors.idp)
92556
- },
92557
- levels: {
92558
- public: containerStyle(spec.lv.public[0], spec.lv.public[1], false, 1.4),
92559
- internal: containerStyle(spec.lv.internal[0], spec.lv.internal[1], false, 1.4),
92560
- restricted: containerStyle(spec.lv.restricted[0], spec.lv.restricted[1], false, 1.4),
92561
- secret: containerStyle(spec.lv.secret[0], spec.lv.secret[1], false, 1.4)
92562
92590
  }
92563
92591
  };
92564
92592
  };
92565
- themes = {
92566
- light: buildTheme({
92593
+ THEME_SPECS = {
92594
+ light: {
92567
92595
  pal: {
92568
92596
  bg: "#ffffff",
92569
92597
  text: "#17202c",
@@ -92611,16 +92639,15 @@ var init_themes = __esm({
92611
92639
  auth: "#b85a30",
92612
92640
  authF: "#f5e6dd",
92613
92641
  idp: "#3a8f8f",
92614
- idpF: "#e0f0f0"
92615
- },
92616
- lv: {
92617
- public: ["#fdeceb", "#d0463f"],
92618
- internal: ["#fef2e2", "#cf9436"],
92619
- restricted: ["#e9f2fb", "#2f7cc4"],
92620
- secret: ["#efe9f7", "#7a55a8"]
92642
+ idpF: "#e0f0f0",
92643
+ fw: "#c0392b",
92644
+ fwF: "#fdecea",
92645
+ authn: "#2f6fb5",
92646
+ authnF: "#e8f1fb"
92621
92647
  }
92622
- }),
92623
- dark: buildTheme({
92648
+ },
92649
+ dark: {
92650
+ dark: true,
92624
92651
  pal: {
92625
92652
  bg: "#1e2530",
92626
92653
  text: "#e6edf3",
@@ -92668,16 +92695,14 @@ var init_themes = __esm({
92668
92695
  auth: "#c96a4a",
92669
92696
  authF: "#332218",
92670
92697
  idp: "#4fafaf",
92671
- idpF: "#1a2e2e"
92672
- },
92673
- lv: {
92674
- public: ["#3a2422", "#c25a54"],
92675
- internal: ["#332a1c", "#c08a44"],
92676
- restricted: ["#1f2a37", "#4a86b8"],
92677
- secret: ["#291f33", "#8a6cb0"]
92698
+ idpF: "#1a2e2e",
92699
+ fw: "#e5675a",
92700
+ fwF: "#33211f",
92701
+ authn: "#6fa8e0",
92702
+ authnF: "#1d2735"
92678
92703
  }
92679
- }),
92680
- slate: buildTheme({
92704
+ },
92705
+ slate: {
92681
92706
  pal: {
92682
92707
  bg: "#f7f9fb",
92683
92708
  text: "#26303c",
@@ -92725,16 +92750,14 @@ var init_themes = __esm({
92725
92750
  auth: "#a85a30",
92726
92751
  authF: "#f0e4dd",
92727
92752
  idp: "#3a8f8f",
92728
- idpF: "#e4f0f0"
92729
- },
92730
- lv: {
92731
- public: ["#f7ece9", "#c05a4a"],
92732
- internal: ["#f3efe6", "#a8823f"],
92733
- restricted: ["#e8eff6", "#3b6ea5"],
92734
- secret: ["#efecf5", "#7d6ba8"]
92753
+ idpF: "#e4f0f0",
92754
+ fw: "#bd4b3c",
92755
+ fwF: "#f9e9e6",
92756
+ authn: "#3a6ea5",
92757
+ authnF: "#e9f0f8"
92735
92758
  }
92736
- }),
92737
- sand: buildTheme({
92759
+ },
92760
+ sand: {
92738
92761
  pal: {
92739
92762
  bg: "#faf6ee",
92740
92763
  text: "#3a2f22",
@@ -92782,16 +92805,14 @@ var init_themes = __esm({
92782
92805
  auth: "#a85a30",
92783
92806
  authF: "#f0e5dd",
92784
92807
  idp: "#3a7a6f",
92785
- idpF: "#e5f0ed"
92786
- },
92787
- lv: {
92788
- public: ["#f7e2da", "#c0562a"],
92789
- internal: ["#f6ecd2", "#b0842e"],
92790
- restricted: ["#e6f0f1", "#3f7a8c"],
92791
- secret: ["#f0e8ef", "#8a5f7a"]
92808
+ idpF: "#e5f0ed",
92809
+ fw: "#b8492f",
92810
+ fwF: "#f8e8e0",
92811
+ authn: "#3a6b9a",
92812
+ authnF: "#e9eff7"
92792
92813
  }
92793
- }),
92794
- contrast: buildTheme({
92814
+ },
92815
+ contrast: {
92795
92816
  pal: {
92796
92817
  bg: "#ffffff",
92797
92818
  text: "#000000",
@@ -92839,16 +92860,15 @@ var init_themes = __esm({
92839
92860
  auth: "#993a1a",
92840
92861
  authF: "#f6e3dd",
92841
92862
  idp: "#005a5a",
92842
- idpF: "#daf0f0"
92843
- },
92844
- lv: {
92845
- public: ["#f9dcd6", "#c0341a"],
92846
- internal: ["#f6e6c8", "#9a6a00"],
92847
- restricted: ["#e0edf7", "#005a9c"],
92848
- secret: ["#eee0f7", "#6a2fa0"]
92863
+ idpF: "#daf0f0",
92864
+ fw: "#a3140a",
92865
+ fwF: "#fbe3df",
92866
+ authn: "#00408f",
92867
+ authnF: "#dfe8fb"
92849
92868
  }
92850
- }),
92851
- nord: buildTheme({
92869
+ },
92870
+ nord: {
92871
+ dark: true,
92852
92872
  pal: {
92853
92873
  bg: "#2e3440",
92854
92874
  text: "#eceff4",
@@ -92896,16 +92916,14 @@ var init_themes = __esm({
92896
92916
  auth: "#c96a4a",
92897
92917
  authF: "#332218",
92898
92918
  idp: "#6fafaf",
92899
- idpF: "#203432"
92900
- },
92901
- lv: {
92902
- public: ["#3a2a2d", "#bf616a"],
92903
- internal: ["#3a3524", "#d0a85f"],
92904
- restricted: ["#2f3a44", "#81a1c1"],
92905
- secret: ["#312a3a", "#a38bbd"]
92919
+ idpF: "#203432",
92920
+ fw: "#bf616a",
92921
+ fwF: "#3b2f34",
92922
+ authn: "#81a1c1",
92923
+ authnF: "#333c4a"
92906
92924
  }
92907
- }),
92908
- solarized: buildTheme({
92925
+ },
92926
+ solarized: {
92909
92927
  pal: {
92910
92928
  bg: "#fdf6e3",
92911
92929
  text: "#586e75",
@@ -92953,17 +92971,25 @@ var init_themes = __esm({
92953
92971
  auth: "#b85a30",
92954
92972
  authF: "#f5e6dd",
92955
92973
  idp: "#2aa198",
92956
- idpF: "#dff0ec"
92957
- },
92958
- lv: {
92959
- public: ["#f7ddd6", "#dc322f"],
92960
- internal: ["#f2e6c8", "#b58900"],
92961
- restricted: ["#e3edf3", "#268bd2"],
92962
- secret: ["#e8e6f2", "#6c71c4"]
92974
+ idpF: "#dff0ec",
92975
+ fw: "#dc322f",
92976
+ fwF: "#f7e3dd",
92977
+ authn: "#268bd2",
92978
+ authnF: "#e3edf5"
92963
92979
  }
92964
- })
92980
+ }
92965
92981
  };
92982
+ themes = Object.fromEntries(
92983
+ Object.entries(THEME_SPECS).map(([name, spec]) => [name, buildTheme(spec)])
92984
+ );
92966
92985
  themeNames = [...Object.keys(themes), "classic", "classic-dark"];
92986
+ RESERVED_THEME_NAMES = [
92987
+ "classic",
92988
+ "classic-dark",
92989
+ "__proto__",
92990
+ "constructor",
92991
+ "prototype"
92992
+ ];
92967
92993
  }
92968
92994
  });
92969
92995
 
@@ -93087,7 +93113,7 @@ function parseFlow(p, sourceToken) {
93087
93113
  model.flows.push(flow);
93088
93114
  }
93089
93115
  function parseElement(p, sourceToken, parent) {
93090
- const { matchToken, advance, reportError, lookAhead, syncToNextLine, model } = p;
93116
+ const { matchToken, advance, reportError, syncToNextLine, model } = p;
93091
93117
  if (!matchToken("id")) {
93092
93118
  reportError(
93093
93119
  `invalid declaration: \`${sourceToken.text}\` alone on this line`,
@@ -93107,84 +93133,66 @@ function parseElement(p, sourceToken, parent) {
93107
93133
  parent: parent ?? void 0
93108
93134
  };
93109
93135
  if (matchToken("str")) element.label = advance().text;
93110
- if (matchToken("lparen")) {
93111
- advance();
93112
- if (matchToken("id")) {
93113
- const token = advance();
93114
- element.attr = {
93115
- value: token.text,
93116
- span: token.span
93117
- };
93118
- } else
93119
- reportError(
93120
- "attribute value expected after `(`",
93121
- lookAhead().span,
93122
- 'e.g. `trust-zone DMZ "DMZ" (public)`'
93123
- );
93124
- if (matchToken("rparen")) advance();
93125
- else
93126
- reportError(
93127
- "`)` expected to close the attribute",
93128
- lookAhead().span,
93129
- 'e.g. `trust-zone DMZ "DMZ" (public)`'
93130
- );
93131
- }
93132
93136
  if (matchToken("lbrace")) {
93133
93137
  advance();
93134
93138
  p.parseElementBody(element);
93135
93139
  }
93136
93140
  (parent ? parent.children : model.elements).push(element);
93137
93141
  }
93138
- function splitEndpoint(token, flow, role) {
93142
+ function splitEndpoint(token, flow, end) {
93139
93143
  const dot = token.text.lastIndexOf(".");
93140
93144
  if (dot <= 0 || dot === token.text.length - 1) return null;
93141
93145
  const base = token.text.slice(0, dot);
93142
- const side = token.text.slice(dot + 1);
93146
+ const suffix = token.text.slice(dot + 1);
93143
93147
  return {
93144
93148
  flow,
93145
- role,
93149
+ end,
93146
93150
  raw: token.text,
93147
93151
  rawSpan: token.span,
93148
93152
  base,
93149
93153
  baseSpan: { line: token.span.line, col: token.span.col, len: base.length },
93150
- side,
93151
- sideSpan: { line: token.span.line, col: token.span.col + dot + 1, len: side.length }
93154
+ suffix,
93155
+ suffixSpan: { line: token.span.line, col: token.span.col + dot + 1, len: suffix.length }
93152
93156
  };
93153
93157
  }
93154
- function resolveAttachSides(splits, model, diagnostics) {
93158
+ function resolveEndpointSuffixes(splits, model, diagnostics) {
93155
93159
  for (const split of splits) {
93160
+ const isSide = ATTACH_SIDES.includes(split.suffix);
93161
+ const isRole = ATTACH_ROLES.includes(split.suffix);
93156
93162
  if (model.index.has(split.raw)) {
93157
- if (ATTACH_SIDES.includes(split.side))
93163
+ if (isSide || isRole)
93158
93164
  diagnostics.push({
93159
93165
  code: "W0571",
93160
93166
  severity: "warning",
93161
- message: `\`${split.raw}\` is a declared element, so \`.${split.side}\` is not read as an attachment side`,
93167
+ message: `\`${split.raw}\` is a declared element, so \`.${split.suffix}\` is not read as ${isSide ? "an attachment side" : "a role"}`,
93162
93168
  span: split.rawSpan,
93163
93169
  note: "a declared id always wins over the `ID.side` reading",
93164
- help: `rename the element if you meant to attach the flow to the ${split.side} side of \`${split.base}\``
93170
+ help: isSide ? `rename the element if you meant to attach the flow to the ${split.suffix} side of \`${split.base}\`` : `rename the element if you meant \`${split.base}\` to be the ${split.suffix} of the queue at the other end`
93165
93171
  });
93166
93172
  continue;
93167
93173
  }
93168
93174
  if (!model.index.has(split.base)) continue;
93169
- const known = ATTACH_SIDES.includes(split.side);
93170
- if (!known)
93175
+ if (!isSide && !isRole)
93171
93176
  diagnostics.push({
93172
93177
  code: "E0223",
93173
93178
  severity: "error",
93174
- message: `unknown attachment side \`${split.side}\``,
93175
- span: split.sideSpan,
93176
- note: "sides are named as the diagram is read",
93177
- help: "use `left`, `right`, `top` or `bottom`, e.g. `APP.right -> DB.left`"
93179
+ message: `unknown endpoint suffix \`${split.suffix}\``,
93180
+ span: split.suffixSpan,
93181
+ note: "an endpoint names either the side it attaches to, as the diagram is read, or its role towards a queue",
93182
+ help: "use `left`, `right`, `top` or `bottom` \u2014 `APP.right -> DB.left` \u2014 or `producer` / `consumer`, e.g. `CAPTURE.producer -> EVENTS`"
93178
93183
  });
93179
- const side = known ? { value: split.side, span: split.sideSpan } : void 0;
93180
- if (split.role === "from") {
93184
+ const side = isSide ? { value: split.suffix, span: split.suffixSpan } : void 0;
93185
+ const role = isRole ? { value: split.suffix, span: split.suffixSpan } : void 0;
93186
+ if (split.end === "from") {
93181
93187
  split.flow.from = split.base;
93182
93188
  split.flow.fromSpan = split.baseSpan;
93183
93189
  split.flow.fromSide = side;
93190
+ split.flow.fromRole = role;
93184
93191
  } else {
93185
93192
  split.flow.to = split.base;
93186
93193
  split.flow.toSpan = split.baseSpan;
93187
93194
  split.flow.toSide = side;
93195
+ split.flow.toRole = role;
93188
93196
  }
93189
93197
  }
93190
93198
  }
@@ -93213,6 +93221,34 @@ function tryOrderEntry(p, parent) {
93213
93221
  parent.order = { value, span: valueToken.span };
93214
93222
  return true;
93215
93223
  }
93224
+ function tryLogoEntry(p, parent) {
93225
+ const { matchToken, advance, reportCoded, save, restore, syncToNextLine } = p;
93226
+ if (!parent || !matchToken("id", "logo")) return false;
93227
+ const mark = save();
93228
+ const keyToken = advance();
93229
+ if (!matchToken("colon")) {
93230
+ restore(mark);
93231
+ return false;
93232
+ }
93233
+ advance();
93234
+ const valueToken = matchToken("id") || matchToken("str") ? advance() : null;
93235
+ if (!valueToken || valueToken.text.length === 0) {
93236
+ reportCoded(
93237
+ "E0105",
93238
+ "`logo` expects a built-in name or a quoted file path",
93239
+ (valueToken ?? keyToken).span,
93240
+ 'e.g. `logo: react`, or `logo: "./logos/acme.svg"` for your own'
93241
+ );
93242
+ syncToNextLine();
93243
+ return true;
93244
+ }
93245
+ parent.logo = {
93246
+ value: valueToken.text,
93247
+ source: valueToken.kind === "str" ? "file" : "builtin",
93248
+ span: valueToken.span
93249
+ };
93250
+ return true;
93251
+ }
93216
93252
  function tryLegendBlock(p) {
93217
93253
  const { matchToken, advance, reportError, lookAhead, skipNewlines, syncToNextLine, model } = p;
93218
93254
  if (!matchToken("id", "legend")) return false;
@@ -93420,6 +93456,7 @@ function parse(src) {
93420
93456
  }
93421
93457
  if (tryStyleBlock(parent)) return;
93422
93458
  if (tryOrderEntry(parser, parent)) return;
93459
+ if (tryLogoEntry(parser, parent)) return;
93423
93460
  if (!parent && tryLegendBlock(parser)) return;
93424
93461
  if (!parent && tryBusinessObject(parser)) return;
93425
93462
  parseFlowOrElement(parser, parent);
@@ -93432,7 +93469,7 @@ function parse(src) {
93432
93469
  for (const [id, element] of indexElementsById(model.elements)) {
93433
93470
  if (!model.index.has(id)) model.index.set(id, element);
93434
93471
  }
93435
- resolveAttachSides(endpointSplits, model, diagnostics);
93472
+ resolveEndpointSuffixes(endpointSplits, model, diagnostics);
93436
93473
  return { model, diags: diagnostics };
93437
93474
  }
93438
93475
  function applyStyleEntry(entry) {
@@ -93664,12 +93701,14 @@ var init_parser = __esm({
93664
93701
  });
93665
93702
 
93666
93703
  // src/views.ts
93667
- var logicalView, applicationView, infrastructureView, SEC_LEVELS, securityView, views;
93704
+ var LINE_STYLES2, logicalView, applicationView, infrastructureView, views;
93668
93705
  var init_views = __esm({
93669
93706
  "src/views.ts"() {
93670
93707
  "use strict";
93708
+ LINE_STYLES2 = ["solid", "dashed", "dotted"];
93671
93709
  logicalView = {
93672
93710
  name: "logical",
93711
+ laneKinds: ["external"],
93673
93712
  kinds: ["actor-group", "actor", "system", "layer", "block", "external"],
93674
93713
  containerKinds: ["actor-group", "system", "layer", "external"],
93675
93714
  // Logical flows carry no technical detail (flowTech* are null below), so the
@@ -93698,6 +93737,16 @@ var init_views = __esm({
93698
93737
  },
93699
93738
  legendFlowLabel: "Functional flow (label = exchanged data)",
93700
93739
  legendFlowLabelFr: "Flux fonctionnel (libell\xE9 = donn\xE9es \xE9chang\xE9es)",
93740
+ legendLineStyles: {
93741
+ solid: "Direct exchange",
93742
+ dashed: "Asynchronous or event-driven exchange",
93743
+ dotted: "Dependency \u2014 no data exchanged"
93744
+ },
93745
+ legendLineStylesFr: {
93746
+ solid: "\xC9change direct",
93747
+ dashed: "\xC9change asynchrone ou \xE9v\xE9nementiel",
93748
+ dotted: "D\xE9pendance \u2014 aucune donn\xE9e \xE9chang\xE9e"
93749
+ },
93701
93750
  flowTechRequired: null,
93702
93751
  flowTechRecommended: null,
93703
93752
  businessObjects: true,
@@ -93792,17 +93841,33 @@ var init_views = __esm({
93792
93841
  };
93793
93842
  applicationView = {
93794
93843
  name: "application",
93844
+ laneKinds: ["external"],
93845
+ hubKinds: ["queue"],
93795
93846
  kinds: [
93796
93847
  "actor-group",
93797
93848
  "actor",
93798
93849
  "system",
93799
93850
  "application",
93800
93851
  "module",
93852
+ "gateway",
93853
+ "auth",
93854
+ "idp",
93801
93855
  "queue",
93802
93856
  "datastore",
93803
93857
  "external"
93804
93858
  ],
93805
93859
  containerKinds: ["actor-group", "system", "application", "external"],
93860
+ // A logo says what a thing is built with, so it belongs to the kinds that run
93861
+ // code. `actor` and `actor-group` are people; `system` is a grouping whose
93862
+ // children carry their own stacks. `gateway`, `auth` and `idp` are left out
93863
+ // on purpose: they are drawn with a corner glyph, and the glyph renderer has
93864
+ // no logo slot — a logo on them would reserve width and draw nothing.
93865
+ logoKinds: ["application", "module", "queue", "datastore", "external"],
93866
+ // The same three kinds as the infrastructure view, drawn the same way: an API
93867
+ // gateway, an auth middleware and an identity provider are containers in
93868
+ // their own right at the application level, and the glyph is what tells them
93869
+ // apart from a plain `application` at a glance.
93870
+ glyphKinds: ["gateway", "auth", "idp"],
93806
93871
  // C4-style `(API_REST, JSON)`: the protocol half is worth tabulating, the port is not.
93807
93872
  // `zoneOf` walks ancestors nearest-first, so an endpoint inside an application
93808
93873
  // still reads `Name (App)`; the system only annotates what sits directly in it.
@@ -93814,6 +93879,9 @@ var init_views = __esm({
93814
93879
  "actor-group": 0,
93815
93880
  system: 1,
93816
93881
  application: 1,
93882
+ gateway: 1,
93883
+ auth: 1,
93884
+ idp: 1,
93817
93885
  queue: 1,
93818
93886
  datastore: 1,
93819
93887
  external: 2
@@ -93824,6 +93892,9 @@ var init_views = __esm({
93824
93892
  system: "System",
93825
93893
  application: "Application",
93826
93894
  module: "Application module",
93895
+ gateway: "Gateway / reverse proxy",
93896
+ auth: "Auth middleware",
93897
+ idp: "Identity provider (IdP)",
93827
93898
  queue: "Message queue / broker",
93828
93899
  datastore: "Datastore / registry",
93829
93900
  external: "External system"
@@ -93834,6 +93905,9 @@ var init_views = __esm({
93834
93905
  system: "Syst\xE8me",
93835
93906
  application: "Application",
93836
93907
  module: "Module applicatif",
93908
+ gateway: "Passerelle / proxy",
93909
+ auth: "Middleware d'authentification",
93910
+ idp: "Fournisseur d'identit\xE9 (IdP)",
93837
93911
  queue: "File de messages / broker",
93838
93912
  datastore: "Entrep\xF4t / r\xE9f\xE9rentiel",
93839
93913
  external: "Syst\xE8me externe"
@@ -93845,6 +93919,16 @@ var init_views = __esm({
93845
93919
  },
93846
93920
  legendFlowLabel: "Application flow \u2014 (protocol, format) under the label",
93847
93921
  legendFlowLabelFr: "Flux applicatif \u2014 (protocole, format) sous le libell\xE9",
93922
+ legendLineStyles: {
93923
+ solid: "Synchronous call (request / response)",
93924
+ dashed: "Asynchronous exchange (message, event)",
93925
+ dotted: "Dependency \u2014 no direct call"
93926
+ },
93927
+ legendLineStylesFr: {
93928
+ solid: "Appel synchrone (requ\xEAte / r\xE9ponse)",
93929
+ dashed: "\xC9change asynchrone (message, \xE9v\xE9nement)",
93930
+ dotted: "D\xE9pendance \u2014 sans appel direct"
93931
+ },
93848
93932
  flowLabelRequired: null,
93849
93933
  flowTechRequired: null,
93850
93934
  flowTechRecommended: {
@@ -93871,7 +93955,7 @@ var init_views = __esm({
93871
93955
  minCounts: [],
93872
93956
  isolatedWarn: {
93873
93957
  code: "W0510",
93874
- kinds: ["module", "queue", "datastore"],
93958
+ kinds: ["module", "gateway", "auth", "idp", "queue", "datastore"],
93875
93959
  message: "isolated element: no incoming or outgoing flow"
93876
93960
  },
93877
93961
  defaults: {
@@ -93893,6 +93977,20 @@ var init_views = __esm({
93893
93977
  fill: "#ffffff",
93894
93978
  stroke: { color: "#5b7a99", style: "solid", width: 1.3 }
93895
93979
  },
93980
+ // Same colours as the infrastructure view's `gateway`, `auth` and `idp`: one
93981
+ // thing drawn one way, whichever view it appears in.
93982
+ gateway: {
93983
+ fill: "#f5e6dd",
93984
+ stroke: { color: "#bf5530", style: "solid", width: 1.6 }
93985
+ },
93986
+ auth: {
93987
+ fill: "#e8f1fb",
93988
+ stroke: { color: "#2f6fb5", style: "solid", width: 1.5 }
93989
+ },
93990
+ idp: {
93991
+ fill: "#e0f0f0",
93992
+ stroke: { color: "#3a8f8f", style: "solid", width: 1.3 }
93993
+ },
93896
93994
  queue: {
93897
93995
  fill: "#f3eef8",
93898
93996
  stroke: { color: "#8a6fae", style: "solid", width: 1.3 }
@@ -93924,6 +94022,18 @@ var init_views = __esm({
93924
94022
  fill: "#252a31",
93925
94023
  stroke: { color: "#5f7f9e", style: "solid", width: 1.3 }
93926
94024
  },
94025
+ gateway: {
94026
+ fill: "#332218",
94027
+ stroke: { color: "#c96a4a", style: "solid", width: 1.6 }
94028
+ },
94029
+ auth: {
94030
+ fill: "#1d2735",
94031
+ stroke: { color: "#6fa8e0", style: "solid", width: 1.5 }
94032
+ },
94033
+ idp: {
94034
+ fill: "#1a2e2e",
94035
+ stroke: { color: "#4fafaf", style: "solid", width: 1.3 }
94036
+ },
93927
94037
  queue: {
93928
94038
  fill: "#2a2433",
93929
94039
  stroke: { color: "#7a5f9e", style: "solid", width: 1.3 }
@@ -93941,19 +94051,27 @@ var init_views = __esm({
93941
94051
  };
93942
94052
  infrastructureView = {
93943
94053
  name: "infrastructure",
94054
+ laneKinds: ["external"],
94055
+ hubKinds: ["queue"],
93944
94056
  kinds: [
93945
94057
  "actor",
94058
+ "device",
93946
94059
  "site",
93947
94060
  "network-zone",
93948
94061
  "server",
93949
94062
  "app-instance",
93950
94063
  "queue",
93951
94064
  "gateway",
94065
+ "firewall",
93952
94066
  "auth",
93953
94067
  "idp",
93954
94068
  "external"
93955
94069
  ],
93956
94070
  containerKinds: ["site", "network-zone", "server"],
94071
+ glyphKinds: ["gateway", "firewall", "auth", "idp", "device"],
94072
+ // A `device` is a flow origin, so it belongs on the entry side with the
94073
+ // actors rather than in a declaration-order band with the zones.
94074
+ ingressKinds: ["actor", "actor-group", "device"],
93957
94075
  // The reference shape: the matrice des flux techniques as an EA dossier expects it.
93958
94076
  matrix: {
93959
94077
  zoneKinds: ["network-zone", "site"],
@@ -93964,24 +94082,28 @@ var init_views = __esm({
93964
94082
  actorLegend: true,
93965
94083
  legendNames: {
93966
94084
  actor: "User / consumer",
94085
+ device: "Device / workstation",
93967
94086
  site: "Site / data center",
93968
94087
  "network-zone": "Network zone",
93969
94088
  server: "Server / VM",
93970
94089
  "app-instance": "Deployed application",
93971
94090
  queue: "Message queue / broker",
93972
94091
  gateway: "Gateway / reverse proxy",
94092
+ firewall: "Firewall",
93973
94093
  auth: "Auth middleware",
93974
94094
  idp: "Identity provider (IdP)",
93975
94095
  external: "External system"
93976
94096
  },
93977
94097
  legendNamesFr: {
93978
94098
  actor: "Utilisateur / consommateur",
94099
+ device: "Poste de travail",
93979
94100
  site: "Site / centre de donn\xE9es",
93980
94101
  "network-zone": "Zone r\xE9seau",
93981
94102
  server: "Serveur / VM",
93982
94103
  "app-instance": "Application d\xE9ploy\xE9e",
93983
94104
  queue: "File de messages / broker",
93984
94105
  gateway: "Passerelle / proxy",
94106
+ firewall: "Pare-feu",
93985
94107
  auth: "Middleware d'authentification",
93986
94108
  idp: "Fournisseur d'identit\xE9 (IdP)",
93987
94109
  external: "Syst\xE8me externe"
@@ -93993,6 +94115,16 @@ var init_views = __esm({
93993
94115
  },
93994
94116
  legendFlowLabel: "Technical flow (protocol, port)",
93995
94117
  legendFlowLabelFr: "Flux technique (protocole, port)",
94118
+ legendLineStyles: {
94119
+ solid: "Permanent link \u2014 nominal traffic",
94120
+ dashed: "Asynchronous or intermittent link",
94121
+ dotted: "Dependency \u2014 outside nominal traffic"
94122
+ },
94123
+ legendLineStylesFr: {
94124
+ solid: "Lien permanent \u2014 trafic nominal",
94125
+ dashed: "Lien asynchrone ou intermittent",
94126
+ dotted: "D\xE9pendance \u2014 hors trafic nominal"
94127
+ },
93996
94128
  flowLabelRequired: null,
93997
94129
  flowTechRecommended: null,
93998
94130
  flowTechRequired: {
@@ -94026,7 +94158,7 @@ var init_views = __esm({
94026
94158
  minCounts: [],
94027
94159
  isolatedWarn: {
94028
94160
  code: "W0510",
94029
- kinds: ["app-instance", "queue", "gateway", "auth", "idp"],
94161
+ kinds: ["app-instance", "queue", "gateway", "firewall", "auth", "idp", "device"],
94030
94162
  message: "isolated element: no incoming or outgoing flow"
94031
94163
  },
94032
94164
  defaults: {
@@ -94043,6 +94175,12 @@ var init_views = __esm({
94043
94175
  fill: "#ffffff",
94044
94176
  stroke: { color: "#55606b", style: "solid", width: 1.5 }
94045
94177
  },
94178
+ // The server's grey: a workstation is hardware like a server is, and the
94179
+ // monitor glyph is what tells the two apart.
94180
+ device: {
94181
+ fill: "#eef0f2",
94182
+ stroke: { color: "#55606b", style: "solid", width: 1.3 }
94183
+ },
94046
94184
  "app-instance": {
94047
94185
  fill: "#fff7e6",
94048
94186
  stroke: { color: "#b08d2a", style: "solid", width: 1.2 }
@@ -94055,9 +94193,13 @@ var init_views = __esm({
94055
94193
  fill: "#f5e6dd",
94056
94194
  stroke: { color: "#bf5530", style: "solid", width: 1.6 }
94057
94195
  },
94196
+ firewall: {
94197
+ fill: "#fdecea",
94198
+ stroke: { color: "#c0392b", style: "solid", width: 1.6 }
94199
+ },
94058
94200
  auth: {
94059
- fill: "#fef3e2",
94060
- stroke: { color: "#d68a2a", style: "solid", width: 1.5 }
94201
+ fill: "#e8f1fb",
94202
+ stroke: { color: "#2f6fb5", style: "solid", width: 1.5 }
94061
94203
  },
94062
94204
  idp: {
94063
94205
  fill: "#e0f0f0",
@@ -94082,6 +94224,10 @@ var init_views = __esm({
94082
94224
  fill: "#252a31",
94083
94225
  stroke: { color: "#6b7885", style: "solid", width: 1.5 }
94084
94226
  },
94227
+ device: {
94228
+ fill: "#242a30",
94229
+ stroke: { color: "#6b7885", style: "solid", width: 1.3 }
94230
+ },
94085
94231
  "app-instance": {
94086
94232
  fill: "#2e2717",
94087
94233
  stroke: { color: "#b08d2a", style: "solid", width: 1.2 }
@@ -94094,9 +94240,13 @@ var init_views = __esm({
94094
94240
  fill: "#332218",
94095
94241
  stroke: { color: "#c96a4a", style: "solid", width: 1.6 }
94096
94242
  },
94243
+ firewall: {
94244
+ fill: "#33211f",
94245
+ stroke: { color: "#e5675a", style: "solid", width: 1.6 }
94246
+ },
94097
94247
  auth: {
94098
- fill: "#332614",
94099
- stroke: { color: "#b88a30", style: "solid", width: 1.5 }
94248
+ fill: "#1d2735",
94249
+ stroke: { color: "#6fa8e0", style: "solid", width: 1.5 }
94100
94250
  },
94101
94251
  idp: {
94102
94252
  fill: "#1a2e2e",
@@ -94108,186 +94258,186 @@ var init_views = __esm({
94108
94258
  }
94109
94259
  }
94110
94260
  };
94111
- SEC_LEVELS = ["public", "internal", "restricted", "secret"];
94112
- securityView = {
94113
- name: "security",
94114
- kinds: ["trust-zone", "security-node", "asset", "actor-group", "actor", "external"],
94115
- containerKinds: ["trust-zone", "actor-group"],
94116
- // Security flows state their encryption (W0561) but no port; the trust zone
94117
- // an endpoint sits in is the point of the view, so it annotates every row.
94118
- matrix: {
94119
- zoneKinds: ["trust-zone"],
94120
- columns: ["num", "source", "dest", "proto", "nature"]
94261
+ views = {
94262
+ logical: logicalView,
94263
+ application: applicationView,
94264
+ infrastructure: infrastructureView
94265
+ };
94266
+ }
94267
+ });
94268
+
94269
+ // src/logos.ts
94270
+ var LOGOS, LOGO_NAMES;
94271
+ var init_logos = __esm({
94272
+ "src/logos.ts"() {
94273
+ "use strict";
94274
+ LOGOS = {
94275
+ angular: {
94276
+ title: "Angular",
94277
+ d: "M16.712 17.711H7.288l-1.204 2.916L12 24l5.916-3.373-1.204-2.916ZM14.692 0l7.832 16.855.814-12.856L14.692 0ZM9.308 0 .662 3.999l.814 12.856L9.308 0Zm-.405 13.93h6.198L12 6.396 8.903 13.93Z",
94278
+ license: "CC-BY-4.0",
94279
+ licenseUrl: "https://creativecommons.org/licenses/by/4.0/legalcode",
94280
+ source: "https://angular.dev/press-kit"
94121
94281
  },
94122
- legendNames: {
94123
- "trust-zone": "Trust zone (sensitivity)",
94124
- "security-node": "Filtering / security node",
94125
- asset: "Sensitive asset",
94126
- "actor-group": "Actor group",
94127
- actor: "Actor",
94128
- external: "Untrusted external"
94282
+ apache: {
94283
+ title: "Apache",
94284
+ d: "M17.805 2.197v.066h.156v.44h.072v-.44h.156v-.066zm.9 0l-.175.353-.172-.353h-.087v.506h.067V2.3l.172.35h.045l.172-.35v.404h.066v-.506zm-4.257 1c-.204.31-.424.66-.66 1.06l-.04.062a44.457 44.457 0 00-1.265 2.29c-.187.36-.38.742-.577 1.146l2.267-.25c.66-.302.955-.578 1.242-.976a15.5 15.5 0 00.23-.342c.23-.363.46-.763.663-1.16.197-.386.37-.767.505-1.11.083-.22.15-.422.198-.6.042-.158.074-.307.1-.45-.884.15-1.965.295-2.668.33zM11.894 7.78l-.077.16c-.078.16-.157.32-.236.488-.086.18-.172.364-.26.552l-.132.287a75.265 75.265 0 00-1.427 3.3c-.163.397-.327.807-.493 1.23-.15.38-.297.765-.45 1.164l-.02.06c-.15.396-.3.802-.453 1.22l-.01.027.72-.08a.213.213 0 01-.042-.006c.863-.106 2.01-.75 2.75-1.547.342-.367.652-.8.94-1.306.213-.377.413-.795.604-1.258.168-.405.328-.843.48-1.318-.196.105-.423.18-.673.235a2.184 2.184 0 01-.273.046c.806-.31 1.314-.905 1.683-1.64a2.816 2.816 0 01-.968.428c-.06.012-.116.022-.174.03l-.043.006h.002c.278-.118.514-.248.718-.403a2.571 2.571 0 00.637-.698l.063-.104.077-.154a8.107 8.107 0 00.367-.85l.03-.088a3.04 3.04 0 00.123-.463.733.733 0 01-.094.065c-.243.145-.66.277-.996.34l.663-.074-.664.073h-.017l-.1.017c.006-.003.01-.006.017-.008l-2.265.25-.013.022zM8.27 16.45c-.117.323-.236.654-.355.992l-.005.015c-.016.046-.032.094-.05.142-.08.227-.15.432-.31.9.264.12.475.435.675.793a1.44 1.44 0 00-.466-.99c1.293.06 2.41-.27 2.99-1.217.05-.084.096-.173.14-.268-.26.333-.59.474-1.2.44 0 0-.004 0-.005.002l.004-.002c.9-.404 1.354-.79 1.754-1.433.094-.153.186-.32.28-.503-.788.81-1.702 1.04-2.664.865l-.72.078a6.43 6.43 0 00-.067.183zM15.42.112c-.376.222-1 .85-1.748 1.763l.686 1.294c.48-.687.97-1.307 1.462-1.836l.058-.062c-.02.02-.04.04-.057.062-.16.176-.644.74-1.375 1.863.703-.035 1.784-.18 2.666-.33.262-1.47-.258-2.142-.258-2.142s-.66-1.07-1.436-.61zm-3.084 6.402a40.253 40.253 0 011.306-2.26l.04-.064c.224-.352.45-.693.677-1.02l-.685-1.293-.157.192c-.197.245-.403.51-.613.79a39.853 39.853 0 00-2.016 2.97l-.022.038.893 1.763c.19-.378.38-.752.575-1.118zm-3.73 8.32c.158-.406.319-.81.483-1.225.156-.394.32-.79.484-1.19a91.133 91.133 0 011.6-3.604l.205-.424c.12-.243.237-.485.36-.724a.125.125 0 01.02-.04l-.895-1.763-.044.07c-.207.34-.414.687-.617 1.042a38.056 38.056 0 00-1.092 2.04l-.094.193a24.573 24.573 0 00-1.258 3.087 18.492 18.492 0 00-.52 1.997l.896 1.77c.117-.317.24-.638.364-.963zm-1.376-.476a13.38 13.38 0 00-.234 1.692c0 .02-.004.04-.005.06-.28-.45-1.03-.888-1.026-.884.537.778.944 1.55 1.005 2.31-.29.058-.684-.027-1.14-.195.475.436.83.556.97.588-.434.03-.89.328-1.346.67.668-.27 1.21-.38 1.596-.29-.61 1.74-1.23 3.655-1.843 5.69a.538.538 0 00.364-.354c.11-.368.84-2.786 1.978-5.965l.097-.27.028-.078c.12-.332.246-.672.374-1.02l.09-.237v-.004L7.24 14.3c-.003.02-.01.04-.012.06z",
94285
+ license: "Apache-2.0",
94286
+ licenseUrl: "https://www.apache.org/licenses/LICENSE-2.0.txt",
94287
+ source: "https://www.apache.org/foundation/press/kit"
94129
94288
  },
94130
- legendNamesFr: {
94131
- "trust-zone": "Zone de confiance (sensibilit\xE9)",
94132
- "security-node": "N\u0153ud de filtrage / s\xE9curit\xE9",
94133
- asset: "Actif sensible",
94134
- "actor-group": "Groupe d'acteurs",
94135
- actor: "Acteur",
94136
- external: "Externe non ma\xEEtris\xE9"
94289
+ apachekafka: {
94290
+ title: "Apache Kafka",
94291
+ d: "M9.71 2.136a1.43 1.43 0 0 0-2.047 0h-.007a1.48 1.48 0 0 0-.421 1.042c0 .41.161.777.422 1.039l.007.007c.257.264.616.426 1.019.426.404 0 .766-.162 1.027-.426l.003-.007c.261-.262.421-.629.421-1.039 0-.408-.159-.777-.421-1.042H9.71zM8.683 22.295c.404 0 .766-.167 1.027-.429l.003-.008c.261-.261.421-.631.421-1.036 0-.41-.159-.778-.421-1.044H9.71a1.42 1.42 0 0 0-1.027-.432 1.4 1.4 0 0 0-1.02.432h-.007c-.26.266-.422.634-.422 1.044 0 .406.161.775.422 1.036l.007.008c.258.262.617.429 1.02.429zm7.89-4.462c.359-.096.683-.33.882-.684l.027-.052a1.47 1.47 0 0 0 .114-1.067 1.454 1.454 0 0 0-.675-.896l-.021-.014a1.425 1.425 0 0 0-1.078-.132c-.36.091-.684.335-.881.686-.2.349-.241.75-.146 1.119.099.363.33.691.675.896h.002c.346.203.737.239 1.101.144zm-6.405-7.342a2.083 2.083 0 0 0-1.485-.627c-.58 0-1.103.242-1.482.627-.378.385-.612.916-.612 1.507s.233 1.124.612 1.514a2.08 2.08 0 0 0 2.967 0c.379-.39.612-.923.612-1.514s-.233-1.122-.612-1.507zm-.835-2.51c.843.141 1.6.552 2.178 1.144h.004c.092.093.182.196.265.299l1.446-.851a3.176 3.176 0 0 1-.047-1.808 3.149 3.149 0 0 1 1.456-1.926l.025-.016a3.062 3.062 0 0 1 2.345-.306c.77.21 1.465.721 1.898 1.482v.002c.431.757.518 1.626.313 2.408a3.145 3.145 0 0 1-1.456 1.928l-.198.118h-.02a3.095 3.095 0 0 1-2.154.201 3.127 3.127 0 0 1-1.514-.944l-1.444.848a4.162 4.162 0 0 1 0 2.879l1.444.846c.413-.47.939-.789 1.514-.944a3.041 3.041 0 0 1 2.371.319l.048.023v.002a3.17 3.17 0 0 1 1.408 1.906 3.215 3.215 0 0 1-.313 2.405l-.026.053-.003-.005a3.147 3.147 0 0 1-1.867 1.436 3.096 3.096 0 0 1-2.371-.318v-.006a3.156 3.156 0 0 1-1.456-1.927 3.175 3.175 0 0 1 .047-1.805l-1.446-.848a3.905 3.905 0 0 1-.265.294l-.004.005a3.938 3.938 0 0 1-2.178 1.138v1.699a3.09 3.09 0 0 1 1.56.862l.002.004c.565.572.914 1.368.914 2.243 0 .873-.35 1.664-.914 2.239l-.002.009a3.1 3.1 0 0 1-2.21.931 3.1 3.1 0 0 1-2.206-.93h-.002v-.009a3.186 3.186 0 0 1-.916-2.239c0-.875.35-1.672.916-2.243v-.004h.002a3.1 3.1 0 0 1 1.558-.862v-1.699a3.926 3.926 0 0 1-2.176-1.138l-.006-.005a4.098 4.098 0 0 1-1.173-2.874c0-1.122.452-2.136 1.173-2.872h.006a3.947 3.947 0 0 1 2.176-1.144V6.289a3.137 3.137 0 0 1-1.558-.864h-.002v-.004a3.192 3.192 0 0 1-.916-2.243c0-.871.35-1.669.916-2.243l.002-.002A3.084 3.084 0 0 1 8.683 0c.861 0 1.641.355 2.21.932v.002h.002c.565.574.914 1.372.914 2.243 0 .876-.35 1.667-.914 2.243l-.002.005a3.142 3.142 0 0 1-1.56.864v1.692zm8.121-1.129l-.012-.019a1.452 1.452 0 0 0-.87-.668 1.43 1.43 0 0 0-1.103.146h.002c-.347.2-.58.529-.677.896-.095.365-.054.768.146 1.119l.007.009c.2.347.519.579.874.673.357.103.755.059 1.098-.144l.019-.009a1.47 1.47 0 0 0 .657-.885 1.493 1.493 0 0 0-.141-1.118",
94292
+ license: "Apache-2.0",
94293
+ licenseUrl: "https://www.apache.org/licenses/LICENSE-2.0.txt",
94294
+ source: "https://apache.org/logos"
94137
94295
  },
94138
- bandTitles: {
94139
- flows: "FLOWS",
94140
- objects: "BUSINESS OBJECTS",
94141
- legend: "LEGEND"
94296
+ apachespark: {
94297
+ title: "Apache Spark",
94298
+ d: "M10.812 0c-.425.013-.845.215-1.196.605a3.593 3.593 0 00-.493.722c-.355.667-.425 1.415-.556 2.143a551.9 551.9 0 00-.726 4.087c-.027.16-.096.227-.244.273C5.83 8.386 4.06 8.94 2.3 9.514c-.387.125-.773.289-1.114.506-1.042.665-1.196 1.753-.415 2.71.346.422.79.715 1.284.936 1.1.49 2.202.976 3.3 1.47.019.01.036.013.053.019h-.004l1.306.535c0 .023.002.045 0 .073-.2 2.03-.39 4.063-.58 6.095-.04.419-.012.831.134 1.23.317.87 1.065 1.148 1.881.701.372-.204.666-.497.937-.818 1.372-1.623 2.746-3.244 4.113-4.872.111-.133.205-.15.363-.098.349.117.697.231 1.045.347h.001c.02.012.045.02.073.03l.142.042c1.248.416 2.68.775 3.929 1.19.4.132.622.164 1.045.098.311-.048.592-.062.828-.236.602-.33.995-.957.988-1.682-.005-.427-.154-.813-.35-1.186-.82-1.556-1.637-3.113-2.461-4.666-.078-.148-.076-.243.037-.375 1.381-1.615 2.756-3.236 4.133-4.855.272-.32.513-.658.653-1.058.308-.878-.09-1.57-1-1.741a2.783 2.783 0 00-1.235.069c-1.974.521-3.947 1.041-5.918 1.57-.175.047-.26.015-.355-.144a353.08 353.08 0 00-2.421-4.018 4.61 4.61 0 00-.652-.849c-.371-.37-.802-.549-1.227-.536zm.172 3.703a.592.592 0 01.189.211c.87 1.446 1.742 2.89 2.609 4.338.07.118.135.16.277.121 1.525-.41 3.052-.813 4.579-1.217.367-.098.735-.193 1.103-.289a.399.399 0 01-.1.2c-1.259 1.48-2.516 2.962-3.779 4.438-.11.13-.12.22-.04.37.937 1.803 1.768 3.309 2.498 4.76l-3.696-1.019c-.538-.18-1.077-.358-1.615-.539-.163-.055-.25-.03-.36.1-1.248 1.488-2.504 2.97-3.759 4.454a.398.398 0 01-.18.132c.035-.378.068-.757.104-1.136.149-1.572.297-3.144.451-4.716-.03-.318.117-.405-.322-.545-1.493-.593-3.346-1.321-4.816-1.905a.595.595 0 01.24-.134c1.797-.57 3.595-1.14 5.394-1.705.127-.04.199-.092.211-.233.013-.148.05-.294.076-.441.241-1.363.483-2.726.726-4.088.068-.386.14-.771.21-1.157z",
94299
+ license: "Apache-2.0",
94300
+ licenseUrl: "https://www.apache.org/licenses/LICENSE-2.0.txt",
94301
+ source: "https://apache.org/logos"
94142
94302
  },
94143
- legendFlowLabel: "Security flow \u2014 cross-zone flows should be filtered and encrypted",
94144
- legendFlowLabelFr: "Flux de s\xE9curit\xE9 \u2014 les flux inter-zones doivent \xEAtre filtr\xE9s et chiffr\xE9s",
94145
- partitions: {},
94146
- partitionByOrder: true,
94147
- flowLabelRequired: {
94148
- code: "E0203",
94149
- message: "flow without a label",
94150
- help: 'add a label describing the exchange: `A -> B : "\u2026" (TLS1.3)`'
94303
+ django: {
94304
+ title: "Django",
94305
+ d: "M11.146 0h3.924v18.166c-2.013.382-3.491.535-5.096.535-4.791 0-7.288-2.166-7.288-6.32 0-4.002 2.65-6.6 6.753-6.6.637 0 1.121.05 1.707.203zm0 9.143a3.894 3.894 0 00-1.325-.204c-1.988 0-3.134 1.223-3.134 3.365 0 2.09 1.096 3.236 3.109 3.236.433 0 .79-.025 1.35-.102V9.142zM21.314 6.06v9.098c0 3.134-.229 4.638-.917 5.937-.637 1.249-1.478 2.039-3.211 2.905l-3.644-1.733c1.733-.815 2.574-1.53 3.109-2.625.561-1.121.739-2.421.739-5.835V6.059h3.924zM17.39.021h3.924v4.026H17.39z"
94151
94306
  },
94152
- flowTechRequired: null,
94153
- flowTechRecommended: null,
94154
- attrSpec: {
94155
- kind: "trust-zone",
94156
- values: SEC_LEVELS,
94157
- code: "E0250",
94158
- message: "trust zone without a valid sensitivity level",
94159
- help: 'set a level in parentheses: `trust-zone DMZ "DMZ" (public)` \u2014 one of public, internal, restricted, secret'
94307
+ docker: {
94308
+ title: "Docker",
94309
+ d: "M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"
94160
94310
  },
94161
- trustOrder: { public: 0, internal: 1, restricted: 2, secret: 3 },
94162
- boundaryLint: {
94163
- code: "W0560",
94164
- nodeKind: "security-node",
94165
- message: "unfiltered trust-boundary crossing",
94166
- help: "route this flow through a `security-node` (firewall/WAF/bastion), or confirm the direct path is intended"
94311
+ dotnet: {
94312
+ title: ".NET",
94313
+ d: "M24 8.77h-2.468v7.565h-1.425V8.77h-2.462V7.53H24zm-6.852 7.565h-4.821V7.53h4.63v1.24h-3.205v2.494h2.953v1.234h-2.953v2.604h3.396zm-6.708 0H8.882L4.78 9.863a2.896 2.896 0 0 1-.258-.51h-.036c.032.189.048.592.048 1.21v5.772H3.157V7.53h1.659l3.965 6.32c.167.261.275.442.323.54h.024c-.04-.233-.06-.629-.06-1.185V7.529h1.372zm-8.703-.693a.868.829 0 0 1-.869.829.868.829 0 0 1-.868-.83.868.829 0 0 1 .868-.828.868.829 0 0 1 .869.829Z"
94167
94314
  },
94168
- crossZoneTechRecommended: {
94169
- code: "W0561",
94170
- message: "cross-zone flow without stated encryption/protocol",
94171
- help: 'add the protocol/encryption on inter-zone flows: `A -> B : "\u2026" (TLS1.3)`'
94315
+ elasticsearch: {
94316
+ title: "Elasticsearch",
94317
+ d: "M13.394 0C8.683 0 4.609 2.716 2.644 6.667h15.641a4.77 4.77 0 0 0 3.073-1.11c.446-.375.864-.785 1.247-1.243l.001-.002A11.974 11.974 0 0 0 13.394 0zM1.804 8.889a12.009 12.009 0 0 0 0 6.222h14.7a3.111 3.111 0 1 0 0-6.222zm.84 8.444C4.61 21.283 8.684 24 13.395 24c3.701 0 7.011-1.677 9.212-4.312l-.001-.002a9.958 9.958 0 0 0-1.247-1.243 4.77 4.77 0 0 0-3.073-1.11z"
94172
94318
  },
94173
- nesting: [
94174
- {
94175
- code: "E0217",
94176
- child: "asset",
94177
- parents: ["trust-zone"],
94178
- message: "sensitive asset outside any trust zone",
94179
- help: "move this `asset` inside a `trust-zone`"
94180
- },
94181
- {
94182
- code: "E0218",
94183
- child: "security-node",
94184
- parents: ["trust-zone"],
94185
- message: "security node outside any trust zone",
94186
- help: "place this `security-node` inside a `trust-zone` (typically the exposed one it protects)"
94187
- },
94188
- {
94189
- code: "E0211",
94190
- child: "actor",
94191
- parents: ["actor-group"],
94192
- message: "actor outside any group",
94193
- help: "move this `actor` inside an `actor-group`"
94194
- }
94195
- ],
94196
- minCounts: [],
94197
- isolatedWarn: {
94198
- code: "W0510",
94199
- kinds: ["asset"],
94200
- message: "isolated element: no incoming or outgoing flow"
94319
+ express: {
94320
+ title: "Express",
94321
+ d: "M12.262 16.666h1.146l6.975-9.325H19.22zm9.778 1.441v.004l-4.334-5.706-.557.74 4.873 6.682H.945V4.173h9.505l5.026 6.7.574-.772-4.374-5.928h.003l-.719-.945H0v17.544h24zM10.917 8.705a3.8 3.8 0 0 0-1.292-1.183q-.796-.45-1.916-.45c-.746 0-1.37.14-1.906.424a3.76 3.76 0 0 0-1.31 1.12 4.9 4.9 0 0 0-.75 1.581 7.17 7.17 0 0 0 0 3.696c.148.567.402 1.101.75 1.573a3.5 3.5 0 0 0 1.31 1.066q.803.39 1.906.389 1.77 0 2.739-.868.966-.867 1.328-2.457h-1.139q-.271 1.084-.977 1.734-.704.651-1.952.65-.812 0-1.392-.342a3.1 3.1 0 0 1-.957-.869 3.5 3.5 0 0 1-.551-1.182 5 5 0 0 1-.17-1.133 9 9 0 0 0-.015-.286 4.5 4.5 0 0 1 .015-.829c.047-.418.147-.83.296-1.223A3.7 3.7 0 0 1 5.54 9.05a2.9 2.9 0 0 1 .922-.742q.541-.28 1.246-.28c.47 0 .869.093 1.23.28q.541.281.922.742.379.461.587 1.057t.225 1.246H5.625l.004.957h6.182a7.3 7.3 0 0 0-.18-1.924 4.9 4.9 0 0 0-.715-1.68z"
94201
94322
  },
94202
- defaults: {
94203
- "trust-zone": {
94204
- fill: "#f5f5f4",
94205
- stroke: { color: "#8a8a85", style: "solid", width: 1.3 }
94206
- },
94207
- "security-node": {
94208
- fill: "#fff7e6",
94209
- stroke: { color: "#c46b2a", style: "solid", width: 1.6 }
94210
- },
94211
- asset: {
94212
- fill: "#ffffff",
94213
- stroke: { color: "#55606b", style: "solid", width: 1.3 }
94214
- },
94215
- "actor-group": {
94216
- fill: "#eef4fb",
94217
- stroke: { color: "#7a9cc4", style: "dashed", width: 1.2 }
94218
- },
94219
- external: {
94220
- fill: "#fdecea",
94221
- stroke: { color: "#d9534f", style: "dashed", width: 1.3 }
94222
- },
94223
- actor: {}
94323
+ fastapi: {
94324
+ title: "FastAPI",
94325
+ d: "M12 .0387C5.3729.0384.0003 5.3931 0 11.9988c-.001 6.6066 5.372 11.9628 12 11.9625 6.628.0003 12.001-5.3559 12-11.9625-.0003-6.6057-5.3729-11.9604-12-11.96m-.829 5.4153h7.55l-7.5805 5.3284h5.1828L5.279 18.5436q2.9466-6.5444 5.892-13.0896"
94224
94326
  },
94225
- defaultsDark: {
94226
- "trust-zone": {
94227
- fill: "#26261f",
94228
- stroke: { color: "#8a8a72", style: "solid", width: 1.3 }
94229
- },
94230
- "security-node": {
94231
- fill: "#2e2717",
94232
- stroke: { color: "#c46b2a", style: "solid", width: 1.6 }
94233
- },
94234
- asset: {
94235
- fill: "#252a31",
94236
- stroke: { color: "#6b7885", style: "solid", width: 1.3 }
94237
- },
94238
- "actor-group": {
94239
- fill: "#232a33",
94240
- stroke: { color: "#5c7fa8", style: "dashed", width: 1.2 }
94241
- },
94242
- external: {
94243
- fill: "#3a2422",
94244
- stroke: { color: "#c25a54", style: "dashed", width: 1.3 }
94245
- },
94246
- actor: {}
94327
+ github: {
94328
+ title: "GitHub",
94329
+ d: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
94247
94330
  },
94248
- levelDefaults: {
94249
- public: {
94250
- fill: "#fdecea",
94251
- stroke: { color: "#d9534f", style: "solid", width: 1.4 }
94252
- },
94253
- internal: {
94254
- fill: "#fff4e5",
94255
- stroke: { color: "#e0a458", style: "solid", width: 1.4 }
94256
- },
94257
- restricted: {
94258
- fill: "#e8f1f8",
94259
- stroke: { color: "#5b8db8", style: "solid", width: 1.4 }
94260
- },
94261
- secret: {
94262
- fill: "#ece8f5",
94263
- stroke: { color: "#7a5fae", style: "solid", width: 1.4 }
94264
- }
94331
+ gitlab: {
94332
+ title: "GitLab",
94333
+ d: "m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"
94265
94334
  },
94266
- levelDefaultsDark: {
94267
- public: {
94268
- fill: "#3a2422",
94269
- stroke: { color: "#c25a54", style: "solid", width: 1.4 }
94270
- },
94271
- internal: {
94272
- fill: "#332a1c",
94273
- stroke: { color: "#c08a44", style: "solid", width: 1.4 }
94274
- },
94275
- restricted: {
94276
- fill: "#1f2a33",
94277
- stroke: { color: "#4a7ba6", style: "solid", width: 1.4 }
94278
- },
94279
- secret: {
94280
- fill: "#2a2433",
94281
- stroke: { color: "#7a5f9e", style: "solid", width: 1.4 }
94282
- }
94335
+ go: {
94336
+ title: "Go",
94337
+ d: "M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z"
94338
+ },
94339
+ googlecloud: {
94340
+ title: "Google Cloud",
94341
+ d: "M12.19 2.38a9.344 9.344 0 0 0-9.234 6.893c.053-.02-.055.013 0 0-3.875 2.551-3.922 8.11-.247 10.941l.006-.007-.007.03a6.717 6.717 0 0 0 4.077 1.356h5.173l.03.03h5.192c6.687.053 9.376-8.605 3.835-12.35a9.365 9.365 0 0 0-2.821-4.552l-.043.043.006-.05A9.344 9.344 0 0 0 12.19 2.38zm-.358 4.146c1.244-.04 2.518.368 3.486 1.15a5.186 5.186 0 0 1 1.862 4.078v.518c3.53-.07 3.53 5.262 0 5.193h-5.193l-.008.009v-.04H6.785a2.59 2.59 0 0 1-1.067-.23h.001a2.597 2.597 0 1 1 3.437-3.437l3.013-3.012A6.747 6.747 0 0 0 8.11 8.24c.018-.01.04-.026.054-.023a5.186 5.186 0 0 1 3.67-1.69z"
94342
+ },
94343
+ grafana: {
94344
+ title: "Grafana",
94345
+ d: "M23.02 10.59a8.578 8.578 0 0 0-.862-3.034 8.911 8.911 0 0 0-1.789-2.445c.337-1.342-.413-2.505-.413-2.505-1.292-.08-2.113.4-2.416.62-.052-.02-.102-.044-.154-.064-.22-.089-.446-.172-.677-.247-.231-.073-.47-.14-.711-.197a9.867 9.867 0 0 0-.875-.161C14.557.753 12.94 0 12.94 0c-1.804 1.145-2.147 2.744-2.147 2.744l-.018.093c-.098.029-.2.057-.298.088-.138.042-.275.094-.413.143-.138.055-.275.107-.41.166a8.869 8.869 0 0 0-1.557.87l-.063-.029c-2.497-.955-4.716.195-4.716.195-.203 2.658.996 4.33 1.235 4.636a11.608 11.608 0 0 0-.607 2.635C1.636 12.677.953 15.014.953 15.014c1.926 2.214 4.171 2.351 4.171 2.351.003-.002.006-.002.006-.005.285.509.615.994.986 1.446.156.19.32.371.488.548-.704 2.009.099 3.68.099 3.68 2.144.08 3.553-.937 3.849-1.173a9.784 9.784 0 0 0 3.164.501h.08l.055-.003.107-.002.103-.005.003.002c1.01 1.44 2.788 1.646 2.788 1.646 1.264-1.332 1.337-2.653 1.337-2.94v-.058c0-.02-.003-.039-.003-.06.265-.187.52-.387.758-.6a7.875 7.875 0 0 0 1.415-1.7c1.43.083 2.437-.885 2.437-.885-.236-1.49-1.085-2.216-1.264-2.354l-.018-.013-.016-.013a.217.217 0 0 1-.031-.02c.008-.092.016-.18.02-.27.011-.162.016-.323.016-.48v-.253l-.005-.098-.008-.135a1.891 1.891 0 0 0-.01-.13c-.003-.042-.008-.083-.013-.125l-.016-.124-.018-.122a6.215 6.215 0 0 0-2.032-3.73 6.015 6.015 0 0 0-3.222-1.46 6.292 6.292 0 0 0-.85-.048l-.107.002h-.063l-.044.003-.104.008a4.777 4.777 0 0 0-3.335 1.695c-.332.4-.592.84-.768 1.297a4.594 4.594 0 0 0-.312 1.817l.003.091c.005.055.007.11.013.164a3.615 3.615 0 0 0 .698 1.82 3.53 3.53 0 0 0 1.827 1.282c.33.098.66.14.971.137.039 0 .078 0 .114-.002l.063-.003c.02 0 .041-.003.062-.003.034-.002.065-.007.099-.01.007 0 .018-.003.028-.003l.031-.005.06-.008a1.18 1.18 0 0 0 .112-.02c.036-.008.072-.013.109-.024a2.634 2.634 0 0 0 .914-.415c.028-.02.056-.041.085-.065a.248.248 0 0 0 .039-.35.244.244 0 0 0-.309-.06l-.078.042c-.09.044-.184.083-.283.116a2.476 2.476 0 0 1-.475.096c-.028.003-.054.006-.083.006l-.083.002c-.026 0-.054 0-.08-.002l-.102-.006h-.012l-.024.006c-.016-.003-.031-.003-.044-.006-.031-.002-.06-.007-.091-.01a2.59 2.59 0 0 1-.724-.213 2.557 2.557 0 0 1-.667-.438 2.52 2.52 0 0 1-.805-1.475 2.306 2.306 0 0 1-.029-.444l.006-.122v-.023l.002-.031c.003-.021.003-.04.005-.06a3.163 3.163 0 0 1 1.352-2.29 3.12 3.12 0 0 1 .937-.43 2.946 2.946 0 0 1 .776-.101h.06l.07.002.045.003h.026l.07.005a4.041 4.041 0 0 1 1.635.49 3.94 3.94 0 0 1 1.602 1.662 3.77 3.77 0 0 1 .397 1.414l.005.076.003.075c.002.026.002.05.002.075 0 .024.003.052 0 .07v.065l-.002.073-.008.174a6.195 6.195 0 0 1-.08.639 5.1 5.1 0 0 1-.267.927 5.31 5.31 0 0 1-.624 1.13 5.052 5.052 0 0 1-3.237 2.014 4.82 4.82 0 0 1-.649.066l-.039.003h-.287a6.607 6.607 0 0 1-1.716-.265 6.776 6.776 0 0 1-3.4-2.274 6.75 6.75 0 0 1-.746-1.15 6.616 6.616 0 0 1-.714-2.596l-.005-.083-.002-.02v-.056l-.003-.073v-.096l-.003-.104v-.07l.003-.163c.008-.22.026-.45.054-.678a8.707 8.707 0 0 1 .28-1.355c.128-.444.286-.872.473-1.277a7.04 7.04 0 0 1 1.456-2.1 5.925 5.925 0 0 1 .953-.763c.169-.111.343-.213.524-.306.089-.05.182-.091.273-.135.047-.02.093-.042.138-.062a7.177 7.177 0 0 1 .714-.267l.145-.045c.049-.015.098-.026.148-.041.098-.029.197-.052.296-.076.049-.013.1-.02.15-.033l.15-.032.151-.028.076-.013.075-.01.153-.024c.057-.01.114-.013.171-.023l.169-.021c.036-.003.073-.008.106-.01l.073-.008.036-.003.042-.002c.057-.003.114-.008.171-.01l.086-.006h.023l.037-.003.145-.007a7.999 7.999 0 0 1 1.708.125 7.917 7.917 0 0 1 2.048.68 8.253 8.253 0 0 1 1.672 1.09l.09.077.089.078c.06.052.114.107.171.159.057.052.112.106.166.16.052.055.107.107.159.164a8.671 8.671 0 0 1 1.41 1.978c.012.026.028.052.04.078l.04.078.075.156c.023.051.05.1.07.153l.065.15a8.848 8.848 0 0 1 .45 1.34.19.19 0 0 0 .201.142.186.186 0 0 0 .172-.184c.01-.246.002-.532-.024-.856z"
94346
+ },
94347
+ graphql: {
94348
+ title: "GraphQL",
94349
+ d: "M12.002 0a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm8.54 4.931a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm0 9.862a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm-8.54 4.931a2.138 2.138 0 1 0 0 4.276 2.138 2.138 0 1 0 0-4.276zm-8.542-4.93a2.138 2.138 0 1 0 0 4.276 2.138 2.138 0 1 0 0-4.277zm0-9.863a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm8.542-3.378L2.953 6.777v10.448l9.049 5.224 9.047-5.224V6.777zm0 1.601 7.66 13.27H4.34zm-1.387.371L3.97 15.037V7.363zm2.774 0 6.646 3.838v7.674zM5.355 17.44h13.293l-6.646 3.836z"
94350
+ },
94351
+ javascript: {
94352
+ title: "JavaScript",
94353
+ d: "M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z",
94354
+ license: "MIT",
94355
+ licenseUrl: "https://spdx.org/licenses/MIT.html",
94356
+ source: "https://github.com/voodootikigod/logo.js/blob/1544bdeed6d618a6cfe4f0650d04ab8d9cfa76d9/js.svg",
94357
+ copyright: "Copyright (c) 2011 Christopher Williams <chris@iterativedesigns.com>"
94358
+ },
94359
+ kotlin: { title: "Kotlin", d: "M24 24H0V0h24L12 12Z" },
94360
+ kubernetes: {
94361
+ title: "Kubernetes",
94362
+ d: "M10.204 14.35l.007.01-.999 2.413a5.171 5.171 0 0 1-2.075-2.597l2.578-.437.004.005a.44.44 0 0 1 .484.606zm-.833-2.129a.44.44 0 0 0 .173-.756l.002-.011L7.585 9.7a5.143 5.143 0 0 0-.73 3.255l2.514-.725.002-.009zm1.145-1.98a.44.44 0 0 0 .699-.337l.01-.005.15-2.62a5.144 5.144 0 0 0-3.01 1.442l2.147 1.523.004-.002zm.76 2.75l.723.349.722-.347.18-.78-.5-.623h-.804l-.5.623.179.779zm1.5-3.095a.44.44 0 0 0 .7.336l.008.003 2.134-1.513a5.188 5.188 0 0 0-2.992-1.442l.148 2.615.002.001zm10.876 5.97l-5.773 7.181a1.6 1.6 0 0 1-1.248.594l-9.261.003a1.6 1.6 0 0 1-1.247-.596l-5.776-7.18a1.583 1.583 0 0 1-.307-1.34L2.1 5.573c.108-.47.425-.864.863-1.073L11.305.513a1.606 1.606 0 0 1 1.385 0l8.345 3.985c.438.209.755.604.863 1.073l2.062 8.955c.108.47-.005.963-.308 1.34zm-3.289-2.057c-.042-.01-.103-.026-.145-.034-.174-.033-.315-.025-.479-.038-.35-.037-.638-.067-.895-.148-.105-.04-.18-.165-.216-.216l-.201-.059a6.45 6.45 0 0 0-.105-2.332 6.465 6.465 0 0 0-.936-2.163c.052-.047.15-.133.177-.159.008-.09.001-.183.094-.282.197-.185.444-.338.743-.522.142-.084.273-.137.415-.242.032-.024.076-.062.11-.089.24-.191.295-.52.123-.736-.172-.216-.506-.236-.745-.045-.034.027-.08.062-.111.088-.134.116-.217.23-.33.35-.246.25-.45.458-.673.609-.097.056-.239.037-.303.033l-.19.135a6.545 6.545 0 0 0-4.146-2.003l-.012-.223c-.065-.062-.143-.115-.163-.25-.022-.268.015-.557.057-.905.023-.163.061-.298.068-.475.001-.04-.001-.099-.001-.142 0-.306-.224-.555-.5-.555-.275 0-.499.249-.499.555l.001.014c0 .041-.002.092 0 .128.006.177.044.312.067.475.042.348.078.637.056.906a.545.545 0 0 1-.162.258l-.012.211a6.424 6.424 0 0 0-4.166 2.003 8.373 8.373 0 0 1-.18-.128c-.09.012-.18.04-.297-.029-.223-.15-.427-.358-.673-.608-.113-.12-.195-.234-.329-.349-.03-.026-.077-.062-.111-.088a.594.594 0 0 0-.348-.132.481.481 0 0 0-.398.176c-.172.216-.117.546.123.737l.007.005.104.083c.142.105.272.159.414.242.299.185.546.338.743.522.076.082.09.226.1.288l.16.143a6.462 6.462 0 0 0-1.02 4.506l-.208.06c-.055.072-.133.184-.215.217-.257.081-.546.11-.895.147-.164.014-.305.006-.48.039-.037.007-.09.02-.133.03l-.004.002-.007.002c-.295.071-.484.342-.423.608.061.267.349.429.645.365l.007-.001.01-.003.129-.029c.17-.046.294-.113.448-.172.33-.118.604-.217.87-.256.112-.009.23.069.288.101l.217-.037a6.5 6.5 0 0 0 2.88 3.596l-.09.218c.033.084.069.199.044.282-.097.252-.263.517-.452.813-.091.136-.185.242-.268.399-.02.037-.045.095-.064.134-.128.275-.034.591.213.71.248.12.556-.007.69-.282v-.002c.02-.039.046-.09.062-.127.07-.162.094-.301.144-.458.132-.332.205-.68.387-.897.05-.06.13-.082.215-.105l.113-.205a6.453 6.453 0 0 0 4.609.012l.106.192c.086.028.18.042.256.155.136.232.229.507.342.84.05.156.074.295.145.457.016.037.043.09.062.129.133.276.442.402.69.282.247-.118.341-.435.213-.71-.02-.039-.045-.096-.065-.134-.083-.156-.177-.261-.268-.398-.19-.296-.346-.541-.443-.793-.04-.13.007-.21.038-.294-.018-.022-.059-.144-.083-.202a6.499 6.499 0 0 0 2.88-3.622c.064.01.176.03.213.038.075-.05.144-.114.28-.104.266.039.54.138.87.256.154.06.277.128.448.173.036.01.088.019.13.028l.009.003.007.001c.297.064.584-.098.645-.365.06-.266-.128-.537-.423-.608zM16.4 9.701l-1.95 1.746v.005a.44.44 0 0 0 .173.757l.003.01 2.526.728a5.199 5.199 0 0 0-.108-1.674A5.208 5.208 0 0 0 16.4 9.7zm-4.013 5.325a.437.437 0 0 0-.404-.232.44.44 0 0 0-.372.233h-.002l-1.268 2.292a5.164 5.164 0 0 0 3.326.003l-1.27-2.296h-.01zm1.888-1.293a.44.44 0 0 0-.27.036.44.44 0 0 0-.214.572l-.003.004 1.01 2.438a5.15 5.15 0 0 0 2.081-2.615l-2.6-.44-.004.005z"
94363
+ },
94364
+ mongodb: {
94365
+ title: "MongoDB",
94366
+ d: "M17.193 9.555c-1.264-5.58-4.252-7.414-4.573-8.115-.28-.394-.53-.954-.735-1.44-.036.495-.055.685-.523 1.184-.723.566-4.438 3.682-4.74 10.02-.282 5.912 4.27 9.435 4.888 9.884l.07.05A73.49 73.49 0 0111.91 24h.481c.114-1.032.284-2.056.51-3.07.417-.296.604-.463.85-.693a11.342 11.342 0 003.639-8.464c.01-.814-.103-1.662-.197-2.218zm-5.336 8.195s0-8.291.275-8.29c.213 0 .49 10.695.49 10.695-.381-.045-.765-1.76-.765-2.405z"
94367
+ },
94368
+ mysql: {
94369
+ title: "MySQL",
94370
+ d: "M16.405 5.501c-.115 0-.193.014-.274.033v.013h.014c.054.104.146.18.214.273.054.107.1.214.154.32l.014-.015c.094-.066.14-.172.14-.333-.04-.047-.046-.094-.08-.14-.04-.067-.126-.1-.18-.153zM5.77 18.695h-.927a50.854 50.854 0 00-.27-4.41h-.008l-1.41 4.41H2.45l-1.4-4.41h-.01a72.892 72.892 0 00-.195 4.41H0c.055-1.966.192-3.81.41-5.53h1.15l1.335 4.064h.008l1.347-4.064h1.095c.242 2.015.384 3.86.428 5.53zm4.017-4.08c-.378 2.045-.876 3.533-1.492 4.46-.482.716-1.01 1.073-1.583 1.073-.153 0-.34-.046-.566-.138v-.494c.11.017.24.026.386.026.268 0 .483-.075.647-.222.197-.18.295-.382.295-.605 0-.155-.077-.47-.23-.944L6.23 14.615h.91l.727 2.36c.164.536.233.91.205 1.123.4-1.064.678-2.227.835-3.483zm12.325 4.08h-2.63v-5.53h.885v4.85h1.745zm-3.32.135l-1.016-.5c.09-.076.177-.158.255-.25.433-.506.648-1.258.648-2.253 0-1.83-.718-2.746-2.155-2.746-.704 0-1.254.232-1.65.697-.43.508-.646 1.256-.646 2.245 0 .972.19 1.686.574 2.14.35.41.877.615 1.583.615.264 0 .506-.033.725-.098l1.325.772.36-.622zM15.5 17.588c-.225-.36-.337-.94-.337-1.736 0-1.393.424-2.09 1.27-2.09.443 0 .77.167.977.5.224.362.336.936.336 1.723 0 1.404-.424 2.108-1.27 2.108-.445 0-.77-.167-.978-.5zm-1.658-.425c0 .47-.172.856-.516 1.156-.344.3-.803.45-1.384.45-.543 0-1.064-.172-1.573-.515l.237-.476c.438.22.833.328 1.19.328.332 0 .593-.073.783-.22a.754.754 0 00.3-.615c0-.33-.23-.61-.648-.845-.388-.213-1.163-.657-1.163-.657-.422-.307-.632-.636-.632-1.177 0-.45.157-.81.47-1.085.315-.278.72-.415 1.22-.415.512 0 .98.136 1.4.41l-.213.476a2.726 2.726 0 00-1.064-.23c-.283 0-.502.068-.654.206a.685.685 0 00-.248.524c0 .328.234.61.666.85.393.215 1.187.67 1.187.67.433.305.648.63.648 1.168zm9.382-5.852c-.535-.014-.95.04-1.297.188-.1.04-.26.04-.274.167.055.053.063.14.11.214.08.134.218.313.346.407.14.11.28.216.427.31.26.16.555.255.81.416.145.094.293.213.44.313.073.05.12.14.214.172v-.02c-.046-.06-.06-.147-.105-.214-.067-.067-.134-.127-.2-.193a3.223 3.223 0 00-.695-.675c-.214-.146-.682-.35-.77-.595l-.013-.014c.146-.013.32-.066.46-.106.227-.06.435-.047.67-.106.106-.027.213-.06.32-.094v-.06c-.12-.12-.21-.283-.334-.395a8.867 8.867 0 00-1.104-.823c-.21-.134-.476-.22-.697-.334-.08-.04-.214-.06-.26-.127-.12-.146-.19-.34-.275-.514a17.69 17.69 0 01-.547-1.163c-.12-.262-.193-.523-.34-.763-.69-1.137-1.437-1.826-2.586-2.5-.247-.14-.543-.2-.856-.274-.167-.008-.334-.02-.5-.027-.11-.047-.216-.174-.31-.235-.38-.24-1.364-.76-1.644-.072-.18.434.267.862.422 1.082.115.153.26.328.34.5.047.116.06.235.107.356.106.294.207.622.347.897.073.14.153.287.247.413.054.073.146.107.167.227-.094.136-.1.334-.154.5-.24.757-.146 1.693.194 2.25.107.166.362.534.703.393.3-.12.234-.5.32-.835.02-.08.007-.133.048-.187v.015c.094.188.188.367.274.555.206.328.566.668.867.895.16.12.287.328.487.402v-.02h-.015c-.043-.058-.1-.086-.154-.133a3.445 3.445 0 01-.35-.4 8.76 8.76 0 01-.747-1.218c-.11-.21-.202-.436-.29-.643-.04-.08-.04-.2-.107-.24-.1.146-.247.273-.32.453-.127.288-.14.642-.188 1.01-.027.007-.014 0-.027.014-.214-.052-.287-.274-.367-.46-.2-.475-.233-1.238-.06-1.785.047-.14.247-.582.167-.716-.042-.127-.174-.2-.247-.303a2.478 2.478 0 01-.24-.427c-.16-.374-.24-.788-.414-1.162-.08-.173-.22-.354-.334-.513-.127-.18-.267-.307-.368-.52-.033-.073-.08-.194-.027-.274.014-.054.042-.075.094-.09.088-.072.335.022.422.062.247.1.455.194.662.334.094.066.195.193.315.226h.14c.214.047.455.014.655.073.355.114.675.28.962.46a5.953 5.953 0 012.085 2.286c.08.154.115.295.188.455.14.33.313.663.455.982.14.315.275.636.476.897.1.14.502.213.682.286.133.06.34.115.46.188.23.14.454.3.67.454.11.076.443.243.463.378z"
94371
+ },
94372
+ natsdotio: {
94373
+ title: "NATS.io",
94374
+ d: "M12.004 0H.404v18.807h9.938l1.714 1.602v-.026L15.966 24v-5.193h7.63V0H12.003zm7.578 14.45H15.38L6.898 6.519v7.93H4.116V4.376h4.349l8.344 7.784V4.375h2.773V14.45z"
94375
+ },
94376
+ nextdotjs: {
94377
+ title: "Next.js",
94378
+ d: "M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z"
94379
+ },
94380
+ nginx: {
94381
+ title: "NGINX",
94382
+ d: "M12 0L1.605 6v12L12 24l10.395-6V6L12 0zm6 16.59c0 .705-.646 1.29-1.529 1.29-.631 0-1.351-.255-1.801-.81l-6-7.141v6.66c0 .721-.57 1.29-1.274 1.29H7.32c-.721 0-1.29-.6-1.29-1.29V7.41c0-.705.63-1.29 1.5-1.29.646 0 1.38.255 1.83.81l5.97 7.141V7.41c0-.721.6-1.29 1.29-1.29h.075c.72 0 1.29.6 1.29 1.29v9.18H18z"
94383
+ },
94384
+ nodedotjs: {
94385
+ title: "Node.js",
94386
+ d: "M11.998,24c-0.321,0-0.641-0.084-0.922-0.247l-2.936-1.737c-0.438-0.245-0.224-0.332-0.08-0.383 c0.585-0.203,0.703-0.25,1.328-0.604c0.065-0.037,0.151-0.023,0.218,0.017l2.256,1.339c0.082,0.045,0.197,0.045,0.272,0l8.795-5.076 c0.082-0.047,0.134-0.141,0.134-0.238V6.921c0-0.099-0.053-0.192-0.137-0.242l-8.791-5.072c-0.081-0.047-0.189-0.047-0.271,0 L3.075,6.68C2.99,6.729,2.936,6.825,2.936,6.921v10.15c0,0.097,0.054,0.189,0.139,0.235l2.409,1.392 c1.307,0.654,2.108-0.116,2.108-0.89V7.787c0-0.142,0.114-0.253,0.256-0.253h1.115c0.139,0,0.255,0.112,0.255,0.253v10.021 c0,1.745-0.95,2.745-2.604,2.745c-0.508,0-0.909,0-2.026-0.551L2.28,18.675c-0.57-0.329-0.922-0.945-0.922-1.604V6.921 c0-0.659,0.353-1.275,0.922-1.603l8.795-5.082c0.557-0.315,1.296-0.315,1.848,0l8.794,5.082c0.57,0.329,0.924,0.944,0.924,1.603 v10.15c0,0.659-0.354,1.273-0.924,1.604l-8.794,5.078C12.643,23.916,12.324,24,11.998,24z M19.099,13.993 c0-1.9-1.284-2.406-3.987-2.763c-2.731-0.361-3.009-0.548-3.009-1.187c0-0.528,0.235-1.233,2.258-1.233 c1.807,0,2.473,0.389,2.747,1.607c0.024,0.115,0.129,0.199,0.247,0.199h1.141c0.071,0,0.138-0.031,0.186-0.081 c0.048-0.054,0.074-0.123,0.067-0.196c-0.177-2.098-1.571-3.076-4.388-3.076c-2.508,0-4.004,1.058-4.004,2.833 c0,1.925,1.488,2.457,3.895,2.695c2.88,0.282,3.103,0.703,3.103,1.269c0,0.983-0.789,1.402-2.642,1.402 c-2.327,0-2.839-0.584-3.011-1.742c-0.02-0.124-0.126-0.215-0.253-0.215h-1.137c-0.141,0-0.254,0.112-0.254,0.253 c0,1.482,0.806,3.248,4.655,3.248C17.501,17.007,19.099,15.91,19.099,13.993z"
94387
+ },
94388
+ openjdk: {
94389
+ title: "OpenJDK",
94390
+ d: "M11.915 0 11.7.215C9.515 2.4 7.47 6.39 6.046 10.483c-1.064 1.024-3.633 2.81-3.711 3.551-.093.87 1.746 2.611 1.55 3.235-.198.625-1.304 1.408-1.014 1.939.1.188.823.011 1.277-.491a13.389 13.389 0 0 0-.017 2.14c.076.906.27 1.668.643 2.232.372.563.956.911 1.667.911.397 0 .727-.114 1.024-.264.298-.149.571-.33.91-.5.68-.34 1.634-.666 3.53-.604 1.903.062 2.872.39 3.559.704.687.314 1.15.664 1.925.664.767 0 1.395-.336 1.807-.9.412-.563.631-1.33.72-2.24.06-.623.055-1.32 0-2.066.454.45 1.117.604 1.213.424.29-.53-.816-1.314-1.013-1.937-.198-.624 1.642-2.366 1.549-3.236-.08-.748-2.707-2.568-3.748-3.586C16.428 6.374 14.308 2.394 12.13.215zm.175 6.038a2.95 2.95 0 0 1 2.943 2.942 2.95 2.95 0 0 1-2.943 2.943A2.95 2.95 0 0 1 9.148 8.98a2.95 2.95 0 0 1 2.942-2.942zM8.685 7.983a3.515 3.515 0 0 0-.145.997c0 1.951 1.6 3.55 3.55 3.55 1.95 0 3.55-1.598 3.55-3.55 0-.329-.046-.648-.132-.951.334.095.64.208.915.336a42.699 42.699 0 0 1 2.042 5.829c.678 2.545 1.01 4.92.846 6.607-.082.844-.29 1.51-.606 1.94-.315.431-.713.651-1.315.651-.593 0-.932-.27-1.673-.61-.741-.338-1.825-.694-3.792-.758-1.974-.064-3.073.293-3.821.669-.375.188-.659.373-.911.5s-.466.2-.752.2c-.53 0-.876-.209-1.16-.64-.285-.43-.474-1.101-.545-1.948-.141-1.693.176-4.069.823-6.614a43.155 43.155 0 0 1 1.934-5.783c.348-.167.749-.31 1.192-.425zm-3.382 4.362a.216.216 0 0 1 .13.031c-.166.56-.323 1.116-.463 1.665a33.849 33.849 0 0 0-.547 2.555 3.9 3.9 0 0 0-.2-.39c-.58-1.012-.914-1.642-1.16-2.08.315-.24 1.679-1.755 2.24-1.781zm13.394.01c.562.027 1.926 1.543 2.24 1.783-.246.438-.58 1.068-1.16 2.08a4.428 4.428 0 0 0-.163.309 32.354 32.354 0 0 0-.562-2.49 40.579 40.579 0 0 0-.482-1.652.216.216 0 0 1 .127-.03z",
94391
+ license: "BSD-3-Clause",
94392
+ licenseUrl: "https://spdx.org/licenses/BSD-3-Clause.html",
94393
+ source: "https://github.com/openjdk/duke/blob/master/vector/Agent.svg"
94394
+ },
94395
+ postgresql: {
94396
+ title: "PostgreSQL",
94397
+ d: "M23.5594 14.7228a.5269.5269 0 0 0-.0563-.1191c-.139-.2632-.4768-.3418-1.0074-.2321-1.6533.3411-2.2935.1312-2.5256-.0191 1.342-2.0482 2.445-4.522 3.0411-6.8297.2714-1.0507.7982-3.5237.1222-4.7316a1.5641 1.5641 0 0 0-.1509-.235C21.6931.9086 19.8007.0248 17.5099.0005c-1.4947-.0158-2.7705.3461-3.1161.4794a9.449 9.449 0 0 0-.5159-.0816 8.044 8.044 0 0 0-1.3114-.1278c-1.1822-.0184-2.2038.2642-3.0498.8406-.8573-.3211-4.7888-1.645-7.2219.0788C.9359 2.1526.3086 3.8733.4302 6.3043c.0409.818.5069 3.334 1.2423 5.7436.4598 1.5065.9387 2.7019 1.4334 3.582.553.9942 1.1259 1.5933 1.7143 1.7895.4474.1491 1.1327.1441 1.8581-.7279.8012-.9635 1.5903-1.8258 1.9446-2.2069.4351.2355.9064.3625 1.39.3772a.0569.0569 0 0 0 .0004.0041 11.0312 11.0312 0 0 0-.2472.3054c-.3389.4302-.4094.5197-1.5002.7443-.3102.064-1.1344.2339-1.1464.8115-.0025.1224.0329.2309.0919.3268.2269.4231.9216.6097 1.015.6331 1.3345.3335 2.5044.092 3.3714-.6787-.017 2.231.0775 4.4174.3454 5.0874.2212.5529.7618 1.9045 2.4692 1.9043.2505 0 .5263-.0291.8296-.0941 1.7819-.3821 2.5557-1.1696 2.855-2.9059.1503-.8707.4016-2.8753.5388-4.1012.0169-.0703.0357-.1207.057-.1362.0007-.0005.0697-.0471.4272.0307a.3673.3673 0 0 0 .0443.0068l.2539.0223.0149.001c.8468.0384 1.9114-.1426 2.5312-.4308.6438-.2988 1.8057-1.0323 1.5951-1.6698zM2.371 11.8765c-.7435-2.4358-1.1779-4.8851-1.2123-5.5719-.1086-2.1714.4171-3.6829 1.5623-4.4927 1.8367-1.2986 4.8398-.5408 6.108-.13-.0032.0032-.0066.0061-.0098.0094-2.0238 2.044-1.9758 5.536-1.9708 5.7495-.0002.0823.0066.1989.0162.3593.0348.5873.0996 1.6804-.0735 2.9184-.1609 1.1504.1937 2.2764.9728 3.0892.0806.0841.1648.1631.2518.2374-.3468.3714-1.1004 1.1926-1.9025 2.1576-.5677.6825-.9597.5517-1.0886.5087-.3919-.1307-.813-.5871-1.2381-1.3223-.4796-.839-.9635-2.0317-1.4155-3.5126zm6.0072 5.0871c-.1711-.0428-.3271-.1132-.4322-.1772.0889-.0394.2374-.0902.4833-.1409 1.2833-.2641 1.4815-.4506 1.9143-1.0002.0992-.126.2116-.2687.3673-.4426a.3549.3549 0 0 0 .0737-.1298c.1708-.1513.2724-.1099.4369-.0417.156.0646.3078.26.3695.4752.0291.1016.0619.2945-.0452.4444-.9043 1.2658-2.2216 1.2494-3.1676 1.0128zm2.094-3.988-.0525.141c-.133.3566-.2567.6881-.3334 1.003-.6674-.0021-1.3168-.2872-1.8105-.8024-.6279-.6551-.9131-1.5664-.7825-2.5004.1828-1.3079.1153-2.4468.079-3.0586-.005-.0857-.0095-.1607-.0122-.2199.2957-.2621 1.6659-.9962 2.6429-.7724.4459.1022.7176.4057.8305.928.5846 2.7038.0774 3.8307-.3302 4.7363-.084.1866-.1633.3629-.2311.5454zm7.3637 4.5725c-.0169.1768-.0358.376-.0618.5959l-.146.4383a.3547.3547 0 0 0-.0182.1077c-.0059.4747-.054.6489-.115.8693-.0634.2292-.1353.4891-.1794 1.0575-.11 1.4143-.8782 2.2267-2.4172 2.5565-1.5155.3251-1.7843-.4968-2.0212-1.2217a6.5824 6.5824 0 0 0-.0769-.2266c-.2154-.5858-.1911-1.4119-.1574-2.5551.0165-.5612-.0249-1.9013-.3302-2.6462.0044-.2932.0106-.5909.019-.8918a.3529.3529 0 0 0-.0153-.1126 1.4927 1.4927 0 0 0-.0439-.208c-.1226-.4283-.4213-.7866-.7797-.9351-.1424-.059-.4038-.1672-.7178-.0869.067-.276.1831-.5875.309-.9249l.0529-.142c.0595-.16.134-.3257.213-.5012.4265-.9476 1.0106-2.2453.3766-5.1772-.2374-1.0981-1.0304-1.6343-2.2324-1.5098-.7207.0746-1.3799.3654-1.7088.5321a5.6716 5.6716 0 0 0-.1958.1041c.0918-1.1064.4386-3.1741 1.7357-4.4823a4.0306 4.0306 0 0 1 .3033-.276.3532.3532 0 0 0 .1447-.0644c.7524-.5706 1.6945-.8506 2.802-.8325.4091.0067.8017.0339 1.1742.081 1.939.3544 3.2439 1.4468 4.0359 2.3827.8143.9623 1.2552 1.9315 1.4312 2.4543-1.3232-.1346-2.2234.1268-2.6797.779-.9926 1.4189.543 4.1729 1.2811 5.4964.1353.2426.2522.4522.2889.5413.2403.5825.5515.9713.7787 1.2552.0696.087.1372.1714.1885.245-.4008.1155-1.1208.3825-1.0552 1.717-.0123.1563-.0423.4469-.0834.8148-.0461.2077-.0702.4603-.0994.7662zm.8905-1.6211c-.0405-.8316.2691-.9185.5967-1.0105a2.8566 2.8566 0 0 0 .135-.0406 1.202 1.202 0 0 0 .1342.103c.5703.3765 1.5823.4213 3.0068.1344-.2016.1769-.5189.3994-.9533.6011-.4098.1903-1.0957.333-1.7473.3636-.7197.0336-1.0859-.0807-1.1721-.151zm.5695-9.2712c-.0059.3508-.0542.6692-.1054 1.0017-.055.3576-.112.7274-.1264 1.1762-.0142.4368.0404.8909.0932 1.3301.1066.887.216 1.8003-.2075 2.7014a3.5272 3.5272 0 0 1-.1876-.3856c-.0527-.1276-.1669-.3326-.3251-.6162-.6156-1.1041-2.0574-3.6896-1.3193-4.7446.3795-.5427 1.3408-.5661 2.1781-.463zm.2284 7.0137a12.3762 12.3762 0 0 0-.0853-.1074l-.0355-.0444c.7262-1.1995.5842-2.3862.4578-3.4385-.0519-.4318-.1009-.8396-.0885-1.2226.0129-.4061.0666-.7543.1185-1.0911.0639-.415.1288-.8443.1109-1.3505.0134-.0531.0188-.1158.0118-.1902-.0457-.4855-.5999-1.938-1.7294-3.253-.6076-.7073-1.4896-1.4972-2.6889-2.0395.5251-.1066 1.2328-.2035 2.0244-.1859 2.0515.0456 3.6746.8135 4.8242 2.2824a.908.908 0 0 1 .0667.1002c.7231 1.3556-.2762 6.2751-2.9867 10.5405zm-8.8166-6.1162c-.025.1794-.3089.4225-.6211.4225a.5821.5821 0 0 1-.0809-.0056c-.1873-.026-.3765-.144-.5059-.3156-.0458-.0605-.1203-.178-.1055-.2844.0055-.0401.0261-.0985.0925-.1488.1182-.0894.3518-.1226.6096-.0867.3163.0441.6426.1938.6113.4186zm7.9305-.4114c.0111.0792-.049.201-.1531.3102-.0683.0717-.212.1961-.4079.2232a.5456.5456 0 0 1-.075.0052c-.2935 0-.5414-.2344-.5607-.3717-.024-.1765.2641-.3106.5611-.352.297-.0414.6111.0088.6356.1851z"
94398
+ },
94399
+ prometheus: {
94400
+ title: "Prometheus",
94401
+ d: "M12 0C5.373 0 0 5.372 0 12c0 6.627 5.373 12 12 12s12-5.373 12-12c0-6.628-5.373-12-12-12zm0 22.46c-1.885 0-3.414-1.26-3.414-2.814h6.828c0 1.553-1.528 2.813-3.414 2.813zm5.64-3.745H6.36v-2.046h11.28v2.046zm-.04-3.098H6.391c-.037-.043-.075-.086-.111-.13-1.155-1.401-1.427-2.133-1.69-2.879-.005-.025 1.4.287 2.395.511 0 0 .513.119 1.262.255-.72-.843-1.147-1.915-1.147-3.01 0-2.406 1.845-4.508 1.18-6.207.648.053 1.34 1.367 1.387 3.422.689-.951.977-2.69.977-3.755 0-1.103.727-2.385 1.454-2.429-.648 1.069.168 1.984.894 4.256.272.854.237 2.29.447 3.201.07-1.892.395-4.652 1.595-5.605-.529 1.2.079 2.702.494 3.424.671 1.164 1.078 2.047 1.078 3.716a4.642 4.642 0 01-1.11 2.996c.792-.149 1.34-.283 1.34-.283l2.573-.502s-.374 1.538-1.81 3.019z"
94402
+ },
94403
+ python: {
94404
+ title: "Python",
94405
+ d: "M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z"
94406
+ },
94407
+ rabbitmq: {
94408
+ title: "RabbitMQ",
94409
+ d: "M23.035 9.601h-7.677a.956.956 0 01-.962-.962V.962a.956.956 0 00-.962-.956H10.56a.956.956 0 00-.962.956V8.64a.956.956 0 01-.962.962H5.762a.956.956 0 01-.961-.962V.962A.956.956 0 003.839 0H.959a.956.956 0 00-.956.962v22.076A.956.956 0 00.965 24h22.07a.956.956 0 00.962-.962V10.58a.956.956 0 00-.962-.98zm-3.86 8.152a1.437 1.437 0 01-1.437 1.443h-1.924a1.437 1.437 0 01-1.436-1.443v-1.917a1.437 1.437 0 011.436-1.443h1.924a1.437 1.437 0 011.437 1.443z"
94410
+ },
94411
+ react: {
94412
+ title: "React",
94413
+ d: "M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
94414
+ },
94415
+ redis: {
94416
+ title: "Redis",
94417
+ d: "M22.71 13.145c-1.66 2.092-3.452 4.483-7.038 4.483-3.203 0-4.397-2.825-4.48-5.12.701 1.484 2.073 2.685 4.214 2.63 4.117-.133 6.94-3.852 6.94-7.239 0-4.05-3.022-6.972-8.268-6.972-3.752 0-8.4 1.428-11.455 3.685C2.59 6.937 3.885 9.958 4.35 9.626c2.648-1.904 4.748-3.13 6.784-3.744C8.12 9.244.886 17.05 0 18.425c.1 1.261 1.66 4.648 2.424 4.648.232 0 .431-.133.664-.365a100.49 100.49 0 0 0 5.54-6.765c.222 3.104 1.748 6.898 6.014 6.898 3.819 0 7.604-2.756 9.33-8.965.2-.764-.73-1.361-1.261-.73zm-4.349-5.013c0 1.959-1.926 2.922-3.685 2.922-.941 0-1.664-.247-2.235-.568 1.051-1.592 2.092-3.225 3.21-4.973 1.972.334 2.71 1.43 2.71 2.619z"
94418
+ },
94419
+ spring: {
94420
+ title: "Spring",
94421
+ d: "M21.8537 1.4158a10.4504 10.4504 0 0 1-1.284 2.2471A11.9666 11.9666 0 1 0 3.8518 20.7757l.4445.3951a11.9543 11.9543 0 0 0 19.6316-8.2971c.3457-3.0126-.568-6.8649-2.0743-11.458zM5.5805 20.8745a1.0174 1.0174 0 1 1-.1482-1.4323 1.0396 1.0396 0 0 1 .1482 1.4323zm16.1991-3.5806c-2.9385 3.9263-9.2601 2.5928-13.2852 2.7904 0 0-.7161.0494-1.4323.1481 0 0 .2717-.1234.6174-.2469 2.8398-.9877 4.1732-1.1853 5.9018-2.0743 3.2349-1.6545 6.4698-5.2844 7.1118-9.0379-1.2347 3.6053-4.9881 6.7167-8.3959 7.9761-2.3459.8643-6.5685 1.7039-6.5685 1.7039l-.1729-.0988c-2.8645-1.4076-2.9632-7.6304 2.2718-9.6306 2.2966-.889 4.4696-.395 6.9637-.9877 2.6422-.6174 5.7043-2.5929 6.939-5.1857 1.3828 4.1732 3.062 10.643.0493 14.6434z"
94422
+ },
94423
+ sqlite: {
94424
+ title: "SQLite",
94425
+ d: "M21.678.521c-1.032-.92-2.28-.55-3.513.544a8.71 8.71 0 0 0-.547.535c-2.109 2.237-4.066 6.38-4.674 9.544.237.48.422 1.093.544 1.561a13.044 13.044 0 0 1 .164.703s-.019-.071-.096-.296l-.05-.146a1.689 1.689 0 0 0-.033-.08c-.138-.32-.518-.995-.686-1.289-.143.423-.27.818-.376 1.176.484.884.778 2.4.778 2.4s-.025-.099-.147-.442c-.107-.303-.644-1.244-.772-1.464-.217.804-.304 1.346-.226 1.478.152.256.296.698.422 1.186.286 1.1.485 2.44.485 2.44l.017.224a22.41 22.41 0 0 0 .056 2.748c.095 1.146.273 2.13.5 2.657l.155-.084c-.334-1.038-.47-2.399-.41-3.967.09-2.398.642-5.29 1.661-8.304 1.723-4.55 4.113-8.201 6.3-9.945-1.993 1.8-4.692 7.63-5.5 9.788-.904 2.416-1.545 4.684-1.931 6.857.666-2.037 2.821-2.912 2.821-2.912s1.057-1.304 2.292-3.166c-.74.169-1.955.458-2.362.629-.6.251-.762.337-.762.337s1.945-1.184 3.613-1.72C21.695 7.9 24.195 2.767 21.678.521m-18.573.543A1.842 1.842 0 0 0 1.27 2.9v16.608a1.84 1.84 0 0 0 1.835 1.834h9.418a22.953 22.953 0 0 1-.052-2.707c-.006-.062-.011-.141-.016-.2a27.01 27.01 0 0 0-.473-2.378c-.121-.47-.275-.898-.369-1.057-.116-.197-.098-.31-.097-.432 0-.12.015-.245.037-.386a9.98 9.98 0 0 1 .234-1.045l.217-.028c-.017-.035-.014-.065-.031-.097l-.041-.381a32.8 32.8 0 0 1 .382-1.194l.2-.019c-.008-.016-.01-.038-.018-.053l-.043-.316c.63-3.28 2.587-7.443 4.8-9.791.066-.069.133-.128.198-.194Z"
94426
+ },
94427
+ terraform: {
94428
+ title: "Terraform",
94429
+ d: "M1.44 0v7.575l6.561 3.79V3.787zm21.12 4.227l-6.561 3.791v7.574l6.56-3.787zM8.72 4.23v7.575l6.561 3.787V8.018zm0 8.405v7.575L15.28 24v-7.578z"
94430
+ },
94431
+ typescript: {
94432
+ title: "TypeScript",
94433
+ d: "M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z"
94434
+ },
94435
+ vault: {
94436
+ title: "Vault",
94437
+ d: "M0 0l11.955 24L24 0zm13.366 4.827h1.393v1.38h-1.393zm-2.77 5.569H9.22V8.993h1.389zm0-2.087H9.22V6.906h1.389zm0-2.086H9.22V4.819h1.389zm2.087 6.263h-1.377V11.08h1.388zm0-2.09h-1.377V8.993h1.388zm0-2.087h-1.377V6.906h1.388zm0-2.086h-1.377V4.819h1.388zm.683.683h1.393v1.389h-1.393zm0 3.475V8.993h1.389v1.388Z"
94283
94438
  }
94284
94439
  };
94285
- views = {
94286
- logical: logicalView,
94287
- application: applicationView,
94288
- infrastructure: infrastructureView,
94289
- security: securityView
94290
- };
94440
+ LOGO_NAMES = Object.keys(LOGOS);
94291
94441
  }
94292
94442
  });
94293
94443
 
@@ -94313,8 +94463,8 @@ function validate(model) {
94313
94463
  ...checkMissingLabels(elements),
94314
94464
  ...checkNesting(elements, view),
94315
94465
  ...checkFlows(model, view),
94316
- ...checkElementAttributes(elements, view),
94317
- ...checkTrustBoundaries(model, view),
94466
+ ...checkEndpointRoles(model, view),
94467
+ ...checkLogos(elements, view),
94318
94468
  ...checkBusinessObjects(model, view),
94319
94469
  ...checkMinimumCounts(elements, model, view),
94320
94470
  ...checkIsolatedElements(model, view, elements)
@@ -94356,6 +94506,48 @@ function checkUnknownKinds(elements, view) {
94356
94506
  }
94357
94507
  return diagnostics;
94358
94508
  }
94509
+ function checkLogos(elements, view) {
94510
+ const diagnostics = [];
94511
+ const logoKinds = view.logoKinds ?? [];
94512
+ for (const element of elements) {
94513
+ const logo = element.logo;
94514
+ if (!logo) continue;
94515
+ if (!logoKinds.includes(element.kind)) {
94516
+ diagnostics.push({
94517
+ code: "E0108",
94518
+ severity: "error",
94519
+ message: `\`${element.kind}\` does not carry a logo`,
94520
+ span: logo.span,
94521
+ note: logoKinds.length ? `the \`${view.name}\` view accepts \`logo\` on: ${logoKinds.join(", ")}` : `the \`${view.name}\` view has no kinds that carry a logo`
94522
+ });
94523
+ continue;
94524
+ }
94525
+ if (logo.source === "builtin") {
94526
+ if (Object.hasOwn(LOGOS, logo.value)) continue;
94527
+ const suggestion = nearest(logo.value, LOGO_NAMES);
94528
+ diagnostics.push({
94529
+ code: "E0107",
94530
+ severity: "error",
94531
+ message: `unknown built-in logo \`${logo.value}\``,
94532
+ span: logo.span,
94533
+ note: `${LOGO_NAMES.length} built-ins available \u2014 run \`cairn logos\` to list them`,
94534
+ help: suggestion !== void 0 ? `did you mean \`${suggestion}\`?` : `use a file instead: \`logo: "./logos/${logo.value}.svg"\``
94535
+ });
94536
+ continue;
94537
+ }
94538
+ if (/^[a-z][a-z0-9+.-]*:/i.test(logo.value) || logo.value.startsWith("//")) {
94539
+ diagnostics.push({
94540
+ code: "E0105",
94541
+ severity: "error",
94542
+ message: `\`logo\` will not fetch \`${logo.value}\``,
94543
+ span: logo.span,
94544
+ note: "a linked logo leaves the diagram depending on a server that can change or vanish",
94545
+ help: 'download it next to the diagram and point at the file: `logo: "./logos/name.svg"`'
94546
+ });
94547
+ }
94548
+ }
94549
+ return diagnostics;
94550
+ }
94359
94551
  function checkMissingLabels(elements) {
94360
94552
  const diagnostics = [];
94361
94553
  for (const element of elements) {
@@ -94390,6 +94582,42 @@ function checkNesting(elements, view) {
94390
94582
  }
94391
94583
  return diagnostics;
94392
94584
  }
94585
+ function checkEndpointRoles(model, view) {
94586
+ const diagnostics = [];
94587
+ const hubKinds = new Set(view.hubKinds ?? []);
94588
+ const isHub = (id) => hubKinds.has(model.index.get(id)?.kind ?? "");
94589
+ for (const flow of model.flows) {
94590
+ for (const [role, hubId, hubSide] of [
94591
+ [flow.fromRole, flow.to, flow.toSide],
94592
+ [flow.toRole, flow.from, flow.fromSide]
94593
+ ]) {
94594
+ if (!role) continue;
94595
+ if (!model.index.has(hubId)) continue;
94596
+ if (!isHub(hubId)) {
94597
+ const kinds = [...hubKinds];
94598
+ diagnostics.push({
94599
+ code: "E0224",
94600
+ severity: "error",
94601
+ message: `\`${role.value}\` needs a ${kinds.join(" or ") || "queue"} at the other end of the flow`,
94602
+ span: role.span,
94603
+ note: `\`${hubId}\` is ${model.index.get(hubId)?.kind ? `a \`${model.index.get(hubId).kind}\`` : "not one"}`,
94604
+ help: "a role says what this element does with a queue \u2014 between two ordinary elements the arrow already says it"
94605
+ });
94606
+ continue;
94607
+ }
94608
+ if (hubSide)
94609
+ diagnostics.push({
94610
+ code: "E0225",
94611
+ severity: "error",
94612
+ message: `\`${hubId}.${hubSide.value}\` and \`${role.value}\` both decide where this flow meets the queue`,
94613
+ span: hubSide.span,
94614
+ note: "`producer` means the queue's left cap and `consumer` its right",
94615
+ help: "keep the role and drop the side, or keep the side and drop the role"
94616
+ });
94617
+ }
94618
+ }
94619
+ return diagnostics;
94620
+ }
94393
94621
  function checkFlows(model, view) {
94394
94622
  const diagnostics = [];
94395
94623
  const isActor = (id) => {
@@ -94447,75 +94675,6 @@ function checkFlows(model, view) {
94447
94675
  }
94448
94676
  return diagnostics;
94449
94677
  }
94450
- function checkElementAttributes(elements, view) {
94451
- const spec = view.attrSpec;
94452
- if (!spec) return [];
94453
- const diagnostics = [];
94454
- for (const element of elements) {
94455
- if (element.kind !== spec.kind) continue;
94456
- if (!element.attr) {
94457
- diagnostics.push({
94458
- code: spec.code,
94459
- severity: "error",
94460
- message: spec.message + ` (\`${element.id}\`)`,
94461
- span: element.idSpan,
94462
- help: spec.help
94463
- });
94464
- } else if (!spec.values.includes(element.attr.value)) {
94465
- const suggestion = nearest(element.attr.value, spec.values);
94466
- diagnostics.push({
94467
- code: spec.code,
94468
- severity: "error",
94469
- message: `invalid ${spec.kind} value \`${element.attr.value}\` (\`${element.id}\`)`,
94470
- span: element.attr.span,
94471
- note: `allowed: ${spec.values.join(", ")}`,
94472
- help: suggestion ? `did you mean \`${suggestion}\`?` : spec.help
94473
- });
94474
- }
94475
- }
94476
- return diagnostics;
94477
- }
94478
- function checkTrustBoundaries(model, view) {
94479
- if (!view.boundaryLint && !view.crossZoneTechRecommended) return [];
94480
- const diagnostics = [];
94481
- const zoneOf2 = (id) => {
94482
- for (let ancestor = model.index.get(id)?.parent; ancestor; ancestor = ancestor.parent)
94483
- if (ancestor.kind === "trust-zone") return ancestor;
94484
- return void 0;
94485
- };
94486
- const trustLevelOf = (id) => {
94487
- const level = zoneOf2(id)?.attr?.value;
94488
- return level && view.trustOrder?.[level] !== void 0 ? view.trustOrder[level] : -1;
94489
- };
94490
- const lint = view.boundaryLint;
94491
- const isSecurityNode = (id) => lint !== void 0 && model.index.get(id)?.kind === lint.nodeKind;
94492
- for (const flow of model.flows) {
94493
- if (!model.index.has(flow.from) || !model.index.has(flow.to)) continue;
94494
- const crossesZone = zoneOf2(flow.from) !== zoneOf2(flow.to);
94495
- const boundaryViolation = lint !== void 0 && trustLevelOf(flow.to) > trustLevelOf(flow.from) && !isSecurityNode(flow.from) && !isSecurityNode(flow.to);
94496
- if (boundaryViolation) {
94497
- diagnostics.push({
94498
- code: lint.code,
94499
- severity: "warning",
94500
- message: lint.message,
94501
- span: flow.span,
94502
- note: `flow enters a more-trusted zone without passing a \`${lint.nodeKind}\``,
94503
- help: lint.help
94504
- });
94505
- }
94506
- if (view.crossZoneTechRecommended && crossesZone && !flow.tech?.protocol) {
94507
- diagnostics.push({
94508
- code: view.crossZoneTechRecommended.code,
94509
- severity: "warning",
94510
- message: view.crossZoneTechRecommended.message,
94511
- span: flow.span,
94512
- note: "inter-zone flow \u2014 state how the traffic is protected",
94513
- help: view.crossZoneTechRecommended.help
94514
- });
94515
- }
94516
- }
94517
- return diagnostics;
94518
- }
94519
94678
  function checkForbiddenBusinessObjects(model, view) {
94520
94679
  const diagnostics = [];
94521
94680
  for (const businessObject of model.businessObjects) {
@@ -94674,6 +94833,7 @@ var init_validator = __esm({
94674
94833
  "use strict";
94675
94834
  init_views();
94676
94835
  init_element_tree();
94836
+ init_logos();
94677
94837
  }
94678
94838
  });
94679
94839
 
@@ -94780,13 +94940,15 @@ function wrapText(text, maxChars) {
94780
94940
  if (currentLine) lines.push(currentLine);
94781
94941
  return lines.join("\n");
94782
94942
  }
94783
- var DEFAULT_FONT_SIZE_NODE, FONT_SIZE_BASE, CHAR_WIDTH, fontSizes, measure, CHIP_HEIGHT, chipW, techText, flowLabelBox, nodeSize;
94943
+ var DEFAULT_FONT_SIZE_NODE, FONT_SIZE_BASE, CHAR_WIDTH, GLYPH_GUTTER, LOGO_GUTTER, fontSizes, measure, CHIP_HEIGHT, chipW, techText, flowLabelBox, nodeSize;
94784
94944
  var init_text_metrics = __esm({
94785
94945
  "src/text-metrics.ts"() {
94786
94946
  "use strict";
94787
94947
  DEFAULT_FONT_SIZE_NODE = 12.5;
94788
94948
  FONT_SIZE_BASE = 12.5;
94789
94949
  CHAR_WIDTH = 0.56;
94950
+ GLYPH_GUTTER = 26;
94951
+ LOGO_GUTTER = 30;
94790
94952
  fontSizes = (base) => {
94791
94953
  const scale = base / FONT_SIZE_BASE;
94792
94954
  return {
@@ -94823,11 +94985,11 @@ var init_text_metrics = __esm({
94823
94985
  height: measured.height + (tech ? 12 * scale : 0) + (chipNames.length ? CHIP_HEIGHT * scale : 0)
94824
94986
  };
94825
94987
  };
94826
- nodeSize = (kind, label, fontSize = DEFAULT_FONT_SIZE_NODE) => {
94988
+ nodeSize = (kind, label, fontSize = DEFAULT_FONT_SIZE_NODE, gutter = 0) => {
94827
94989
  const isActor = kind === "actor";
94828
94990
  const measured = measure(label, isActor ? fontSize - 1.5 : fontSize);
94829
94991
  return {
94830
- width: isActor ? Math.max(64, measured.width + 8) : Math.max(140, measured.width + 16),
94992
+ width: isActor ? Math.max(64, measured.width + 8) : Math.max(140, measured.width + 16 + gutter),
94831
94993
  height: isActor ? 56 + (label.split("\n").length - 1) * 11 : Math.max(46, measured.height + 18)
94832
94994
  };
94833
94995
  };
@@ -94835,12 +94997,13 @@ var init_text_metrics = __esm({
94835
94997
  });
94836
94998
 
94837
94999
  // src/slide-fold.ts
94838
- function foldStyle(model) {
95000
+ function foldStyle(model, view) {
94839
95001
  const businessObjectNames = new Map(model.businessObjects.map((bo) => [bo.id, bo.name]));
94840
95002
  const numbered = model.style.flowText === "numbered";
94841
95003
  const { edge, node, cont, scale } = fontSizes(model.style.font.size);
94842
95004
  return {
94843
95005
  numbered,
95006
+ glyphKinds: new Set(view.glyphKinds ?? []),
94844
95007
  edge,
94845
95008
  node,
94846
95009
  cont,
@@ -94855,7 +95018,15 @@ function foldStyle(model) {
94855
95018
  })
94856
95019
  };
94857
95020
  }
94858
- function toElkNode(element, containerFontSize, nodeFontSize) {
95021
+ function leafSize(element, style) {
95022
+ return nodeSize(
95023
+ element.kind,
95024
+ element.label ?? element.id,
95025
+ style.node,
95026
+ style.glyphKinds.has(element.kind) ? GLYPH_GUTTER : 0
95027
+ );
95028
+ }
95029
+ function toElkNode(element, style) {
94859
95030
  if (element.children.length) {
94860
95031
  const lineCount = (element.label ?? element.id).split("\n").length;
94861
95032
  return {
@@ -94866,13 +95037,13 @@ function toElkNode(element, containerFontSize, nodeFontSize) {
94866
95037
  labels: [
94867
95038
  {
94868
95039
  text: element.label ?? element.id,
94869
- ...measure(element.label ?? element.id, containerFontSize)
95040
+ ...measure(element.label ?? element.id, style.cont)
94870
95041
  }
94871
95042
  ],
94872
- children: element.children.map((child) => toElkNode(child, containerFontSize, nodeFontSize))
95043
+ children: element.children.map((child) => toElkNode(child, style))
94873
95044
  };
94874
95045
  }
94875
- const size = nodeSize(element.kind, element.label ?? element.id, nodeFontSize);
95046
+ const size = leafSize(element, style);
94876
95047
  return { id: element.id, width: size.width, height: size.height };
94877
95048
  }
94878
95049
  function walkFoldedNodes(elkNode, offset, elementById, syntheticIds) {
@@ -94951,7 +95122,7 @@ function internalFlowLabels(flow, style) {
94951
95122
  ];
94952
95123
  }
94953
95124
  function buildGroupGraph(group, style, fg) {
94954
- const node = toElkNode(group, style.cont, style.node);
95125
+ const node = toElkNode(group, style);
94955
95126
  const nodeChildren = node.children ??= [];
94956
95127
  for (const flow of fg.interFlows) {
94957
95128
  if (fg.rootOf.get(flow.from) === group) {
@@ -95009,7 +95180,7 @@ function buildGroupGraph(group, style, fg) {
95009
95180
  function layoutColumn(elements, style) {
95010
95181
  return elements.map((group) => {
95011
95182
  const blocks = group.children.map((child) => {
95012
- const size = nodeSize(child.kind, child.label ?? child.id, style.node);
95183
+ const size = leafSize(child, style);
95013
95184
  return { element: child, width: size.width, height: size.height, x: 0, y: 0 };
95014
95185
  });
95015
95186
  const columnWidth = Math.max(
@@ -95047,7 +95218,7 @@ function layoutMiddleRows(middles, middleResults, geom, style) {
95047
95218
  let yCursor = 16 + geom.gutterHeight(0);
95048
95219
  middles.forEach((element, index) => {
95049
95220
  const result = middleResults.get(element.id) ?? null;
95050
- const size = result ? { width: result.children[0].width, height: result.children[0].height } : nodeSize(element.kind, element.label ?? element.id, style.node);
95221
+ const size = result ? { width: result.children[0].width, height: result.children[0].height } : leafSize(element, style);
95051
95222
  rows.push({
95052
95223
  element,
95053
95224
  box: { x: geom.xMiddle, y: yCursor, width: size.width, height: size.height },
@@ -95203,7 +95374,7 @@ function connectorLabel(flow, routed, style) {
95203
95374
  }
95204
95375
  async function foldedLayout(model, view, elk) {
95205
95376
  const roots = model.elements;
95206
- const style = foldStyle(model);
95377
+ const style = foldStyle(model, view);
95207
95378
  const partitionOf = (element) => view.partitions[element.kind] ?? 1;
95208
95379
  const sources = roots.filter((element) => partitionOf(element) === 0);
95209
95380
  const middles = roots.filter((element) => partitionOf(element) === 1);
@@ -95250,7 +95421,7 @@ async function foldedLayout(model, view, elk) {
95250
95421
  const widthSource = Math.max(0, ...sourceColumns.map((col) => col.width));
95251
95422
  const widthMiddle = Math.max(
95252
95423
  ...middles.map(
95253
- (element) => element.children.length ? middleResults.get(element.id).children[0].width : nodeSize(element.kind, element.label ?? element.id, style.node).width
95424
+ (element) => element.children.length ? middleResults.get(element.id).children[0].width : leafSize(element, style).width
95254
95425
  )
95255
95426
  );
95256
95427
  const widthSink = Math.max(0, ...sinkColumns.map((col) => col.width));
@@ -96158,13 +96329,34 @@ function compactVertical(scene) {
96158
96329
  }
96159
96330
  scene.height = Math.ceil(heightAfter + bottomMargin);
96160
96331
  }
96161
- var KEEP_GAP, EDGE_MARGIN, MIN_SAVING;
96332
+ function fitCanvas(scene) {
96333
+ let maxX = 0;
96334
+ let maxY = 0;
96335
+ for (const node of scene.nodes) {
96336
+ maxX = Math.max(maxX, node.x + node.width);
96337
+ maxY = Math.max(maxY, node.y + node.height);
96338
+ }
96339
+ for (const edge of scene.edges) {
96340
+ for (const point of edge.pts) {
96341
+ maxX = Math.max(maxX, point.x);
96342
+ maxY = Math.max(maxY, point.y);
96343
+ }
96344
+ for (const label of edge.labels) {
96345
+ maxX = Math.max(maxX, label.x + label.width);
96346
+ maxY = Math.max(maxY, label.y + label.height);
96347
+ }
96348
+ }
96349
+ scene.width = Math.max(scene.width, Math.ceil(maxX) + CANVAS_MARGIN);
96350
+ scene.height = Math.max(scene.height, Math.ceil(maxY) + CANVAS_MARGIN);
96351
+ }
96352
+ var KEEP_GAP, EDGE_MARGIN, MIN_SAVING, CANVAS_MARGIN;
96162
96353
  var init_compact = __esm({
96163
96354
  "src/compact.ts"() {
96164
96355
  "use strict";
96165
96356
  KEEP_GAP = 14;
96166
96357
  EDGE_MARGIN = 22;
96167
96358
  MIN_SAVING = 4;
96359
+ CANVAS_MARGIN = 10;
96168
96360
  }
96169
96361
  });
96170
96362
 
@@ -96288,8 +96480,7 @@ function labelSeatable(blockers, pts, label) {
96288
96480
  function scanLabelSeats(ctx, subj, profile) {
96289
96481
  for (const label of subj.edge.labels) {
96290
96482
  if (!label.width || !label.height) continue;
96291
- if (!labelSeatable(ctx.blockers, subj.pts, label))
96292
- profile.set(`unlabelled:${subj.edge.id}`, 1);
96483
+ if (!labelSeatable(ctx.blockers, subj.pts, label)) profile.set(`unlabelled:${subj.edge.id}`, 1);
96293
96484
  }
96294
96485
  }
96295
96486
  function scanArrowRoom(subj, profile) {
@@ -96362,8 +96553,7 @@ function scanCrossings(a, b, key, profile) {
96362
96553
  if (!end) continue;
96363
96554
  const dx = Math.max(0, end.node.x - hit.x, hit.x - (end.node.x + end.node.width));
96364
96555
  const dy = Math.max(0, end.node.y - hit.y, hit.y - (end.node.y + end.node.height));
96365
- if (dx * dx + dy * dy <= FAN_REACH * FAN_REACH)
96366
- profile.set(`fan:${key}@${end.node.id}`, 2);
96556
+ if (dx * dx + dy * dy <= FAN_REACH * FAN_REACH) profile.set(`fan:${key}@${end.node.id}`, 2);
96367
96557
  }
96368
96558
  }
96369
96559
  }
@@ -96391,8 +96581,7 @@ function scanSharedSeats(a, b, key, profile) {
96391
96581
  }
96392
96582
  const side = vertical ? seatA.node.height : seatA.node.width;
96393
96583
  const need = Math.min(12, (side - 6) / 2);
96394
- if (apart < need * 0.8)
96395
- profile.set(`tight:${key}@${seatA.node.id}|${seatA.side}`, 4);
96584
+ if (apart < need * 0.8) profile.set(`tight:${key}@${seatA.node.id}|${seatA.side}`, 4);
96396
96585
  }
96397
96586
  }
96398
96587
  function scanPairs(ctx, subj, query) {
@@ -96407,7 +96596,11 @@ function scanPairs(ctx, subj, query) {
96407
96596
  if (ids.has(other.id) && other.id < subj.edge.id) continue;
96408
96597
  const overridden = overrides.has(other.id);
96409
96598
  if (farApart(myBounds, ctx.boundsFor(other, theirs, overridden))) continue;
96410
- const them = { edge: other, pts: theirs, ends: ctx.endsFor(other, theirs, overridden) };
96599
+ const them = {
96600
+ edge: other,
96601
+ pts: theirs,
96602
+ ends: ctx.endsFor(other, theirs, overridden)
96603
+ };
96411
96604
  const key = pair(subj.edge.id, other.id);
96412
96605
  scanCrossings(subj, them, key, profile);
96413
96606
  scanParallelRuns(mine, ctx.runsFor(other, theirs, overridden), key, profile);
@@ -98841,8 +99034,8 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98841
99034
  const subject = { ends, edge };
98842
99035
  const out = [];
98843
99036
  const seen = /* @__PURE__ */ new Set();
98844
- const aSides = edge.pinned?.start ? [ends[0].side] : SIDES;
98845
- const bSides = edge.pinned?.end ? [ends[1].side] : SIDES;
99037
+ const aSides = sideFixed(edge, "start") ? [ends[0].side] : SIDES;
99038
+ const bSides = sideFixed(edge, "end") ? [ends[1].side] : SIDES;
98846
99039
  for (const aSide of aSides)
98847
99040
  for (const bSide of bSides) {
98848
99041
  const sides = { a: aSide, b: bSide };
@@ -98901,12 +99094,13 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98901
99094
  }
98902
99095
  for (const edge of scene.edges) if (edge.pts.length >= 2) enforceOrthogonal(edge);
98903
99096
  }
98904
- var ORTHOGONAL_EPSILON, MIN_HUG_SPAN, CHANNEL_INSET, SNAP, JOG_SNAP, SIDE_INSET, MIN_SIDE_INSET, FAN_REACH2, SEAT_OFFSETS, segmentsCross, segmentLength, wouldMerge, seatWithAlong, crossingsOf, SEAT_GRID, SEAT_CLEAR, CHANNEL_MARGIN, CHANNEL_STEP, CHANNEL_CLEAR;
99097
+ var sideFixed, ORTHOGONAL_EPSILON, MIN_HUG_SPAN, CHANNEL_INSET, SNAP, JOG_SNAP, SIDE_INSET, MIN_SIDE_INSET, FAN_REACH2, SEAT_OFFSETS, segmentsCross, segmentLength, wouldMerge, seatWithAlong, crossingsOf, SEAT_GRID, SEAT_CLEAR, CHANNEL_MARGIN, CHANNEL_STEP, CHANNEL_CLEAR;
98905
99098
  var init_edge_tidy = __esm({
98906
99099
  "src/edge-tidy.ts"() {
98907
99100
  "use strict";
98908
99101
  init_readability();
98909
99102
  init_geometry();
99103
+ sideFixed = (edge, end) => !!(edge.pinned?.[end] || edge.hubSided?.[end]);
98910
99104
  ORTHOGONAL_EPSILON = 0.5;
98911
99105
  MIN_HUG_SPAN = 24;
98912
99106
  CHANNEL_INSET = 24;
@@ -99233,7 +99427,8 @@ function computeIngressExternalElements(model) {
99233
99427
  }
99234
99428
  return ingressExternalElements;
99235
99429
  }
99236
- function toElkNode2(element, compact, fonts, root = false) {
99430
+ function toElkNode2(element, sizing, root = false) {
99431
+ const { compact, fonts, glyphKinds } = sizing;
99237
99432
  const { cont: containerFontSize, node: nodeFontSize } = fonts;
99238
99433
  if (element.children.length) {
99239
99434
  const lineCount = (element.label ?? element.id).split("\n").length;
@@ -99250,17 +99445,16 @@ function toElkNode2(element, compact, fonts, root = false) {
99250
99445
  ...measure(element.label ?? element.id, containerFontSize)
99251
99446
  }
99252
99447
  ],
99253
- children: element.children.map(
99254
- (child) => toElkNode2(child, compact, fonts)
99255
- )
99448
+ children: element.children.map((child) => toElkNode2(child, sizing))
99256
99449
  };
99257
99450
  }
99258
99451
  const measured = measure(element.label ?? element.id, nodeFontSize);
99259
99452
  const isActor = element.kind === "actor";
99453
+ const gutter = (glyphKinds.has(element.kind) ? GLYPH_GUTTER : 0) + (element.logo ? LOGO_GUTTER : 0);
99260
99454
  return {
99261
99455
  id: element.id,
99262
99456
  ...element.order && !root ? { layoutOptions: orderOption(element) } : {},
99263
- width: isActor ? Math.max(64, measure(element.label ?? element.id, nodeFontSize - 1.5).width + 8) : Math.max(compact ? 98 : 108, measured.width + (compact ? 10 : 12)),
99457
+ width: isActor ? Math.max(64, measure(element.label ?? element.id, nodeFontSize - 1.5).width + 8) : Math.max(compact ? 98 : 108, measured.width + (compact ? 10 : 12) + gutter),
99264
99458
  height: isActor ? 54 + ((element.label ?? element.id).split("\n").length - 1) * 11 : Math.max(compact ? 36 : 38, measured.height + (compact ? 10 : 12))
99265
99459
  };
99266
99460
  }
@@ -99418,20 +99612,20 @@ function selectionExtras(scene, model) {
99418
99612
  }
99419
99613
  return extra;
99420
99614
  }
99421
- function relayoutVerdict(before, after) {
99615
+ function tallyProfile(profile) {
99422
99616
  const normalize = (key) => key.replace(/@[-\d.,]+$/, "").replace(/:\d+(?=@|$)/, "");
99423
- const tally = (profile) => {
99424
- const out = /* @__PURE__ */ new Map();
99425
- for (const [key, tier] of profile) {
99426
- const id = normalize(key);
99427
- const entry = out.get(id) ?? { tier, count: 0 };
99428
- entry.count++;
99429
- out.set(id, entry);
99430
- }
99431
- return out;
99432
- };
99433
- const was = tally(before);
99434
- const now = tally(after);
99617
+ const out = /* @__PURE__ */ new Map();
99618
+ for (const [key, tier] of profile) {
99619
+ const id = normalize(key);
99620
+ const entry = out.get(id) ?? { tier, count: 0 };
99621
+ entry.count++;
99622
+ out.set(id, entry);
99623
+ }
99624
+ return out;
99625
+ }
99626
+ function relayoutVerdict(before, after) {
99627
+ const was = tallyProfile(before);
99628
+ const now = tallyProfile(after);
99435
99629
  for (let tier = 0; tier < 5; tier++) {
99436
99630
  let gained = false;
99437
99631
  let lost = false;
@@ -99447,6 +99641,62 @@ function relayoutVerdict(before, after) {
99447
99641
  }
99448
99642
  return -1;
99449
99643
  }
99644
+ function noLadderRegression(before, after) {
99645
+ const perTier = (profile) => {
99646
+ const totals = [0, 0, 0, 0, 0];
99647
+ for (const { tier, count } of tallyProfile(profile).values()) totals[tier] += count;
99648
+ return totals;
99649
+ };
99650
+ const was = perTier(before);
99651
+ const now = perTier(after);
99652
+ return now.every((count, tier) => count <= was[tier]);
99653
+ }
99654
+ function nodeCoverage(scene) {
99655
+ if (!scene.nodes.length) return 1;
99656
+ const left = Math.min(...scene.nodes.map((n) => n.x));
99657
+ const top = Math.min(...scene.nodes.map((n) => n.y));
99658
+ const right = Math.max(...scene.nodes.map((n) => n.x + n.width));
99659
+ const bottom = Math.max(...scene.nodes.map((n) => n.y + n.height));
99660
+ const width = right - left;
99661
+ const height = bottom - top;
99662
+ if (width <= 0 || height <= 0) return 1;
99663
+ const COLUMNS = 32;
99664
+ const ROWS = 16;
99665
+ let covered = 0;
99666
+ for (let row = 0; row < ROWS; row++) {
99667
+ for (let column = 0; column < COLUMNS; column++) {
99668
+ const cellLeft = left + column * width / COLUMNS;
99669
+ const cellRight = left + (column + 1) * width / COLUMNS;
99670
+ const cellTop = top + row * height / ROWS;
99671
+ const cellBottom = top + (row + 1) * height / ROWS;
99672
+ if (scene.nodes.some(
99673
+ (n) => n.x < cellRight && n.x + n.width > cellLeft && n.y < cellBottom && n.y + n.height > cellTop
99674
+ ))
99675
+ covered++;
99676
+ }
99677
+ }
99678
+ return covered / (COLUMNS * ROWS);
99679
+ }
99680
+ async function denserLayout(base, elkPass) {
99681
+ const densitySpecs = [
99682
+ { minLayers: true, dense: true },
99683
+ { placement: "LINEAR_SEGMENTS", dense: true }
99684
+ ];
99685
+ const laidOut = await Promise.allSettled(densitySpecs.map((spec) => elkPass.layout(spec)));
99686
+ const areaOf = (scene) => scene.width * scene.height;
99687
+ const baseProfile = elkPass.profile(base);
99688
+ let bestArea = areaOf(base) * DENSITY_GAIN;
99689
+ let winner = null;
99690
+ for (const [index, settled] of laidOut.entries()) {
99691
+ if (settled.status !== "fulfilled") continue;
99692
+ const candidate = elkPass.toScene(settled.value);
99693
+ if (areaOf(candidate) > bestArea) continue;
99694
+ if (!noLadderRegression(baseProfile, elkPass.profile(candidate))) continue;
99695
+ bestArea = areaOf(candidate);
99696
+ winner = { scene: candidate, options: densitySpecs[index] };
99697
+ }
99698
+ return winner;
99699
+ }
99450
99700
  function sideToward(from, to) {
99451
99701
  const dx = centerOf(to).x - centerOf(from).x;
99452
99702
  const dy = centerOf(to).y - centerOf(from).y;
@@ -99509,7 +99759,7 @@ function constrainPorts(graph, scene, flagged, model) {
99509
99759
  }
99510
99760
  function elkPartitionOf(element, index, view, ingressExternal) {
99511
99761
  if (!view.partitionByOrder) return view.partitions[element.kind] ?? 1;
99512
- if (element.kind === "actor" || element.kind === "actor-group") return INGRESS_PARTITION;
99762
+ if ((view.ingressKinds ?? DEFAULT_INGRESS_KINDS).includes(element.kind)) return INGRESS_PARTITION;
99513
99763
  if (element.kind === "external")
99514
99764
  return ingressExternal.has(element.id) ? INGRESS_PARTITION : EGRESS_PARTITION;
99515
99765
  if (view.partitions[element.kind] !== void 0) return 90 + view.partitions[element.kind];
@@ -99533,16 +99783,13 @@ function readingSlots(model, view, ingressExternal) {
99533
99783
  const lowestOf = /* @__PURE__ */ new Map();
99534
99784
  for (const band of new Set(bandOf.values())) {
99535
99785
  const members = model.elements.filter((element) => bandOf.get(element.id) === band);
99536
- const declared = [
99537
- ...new Set(members.filter((m) => m.order).map((m) => m.order.value))
99538
- ].sort((a, b) => a - b);
99786
+ const declared = [...new Set(members.filter((m) => m.order).map((m) => m.order.value))].sort(
99787
+ (a, b) => a - b
99788
+ );
99539
99789
  const lowest = declared.length ? 1 : 0;
99540
99790
  lowestOf.set(band, lowest);
99541
99791
  for (const member of members)
99542
- slotOf.set(
99543
- member.id,
99544
- member.order ? declared.indexOf(member.order.value) + 1 : lowest
99545
- );
99792
+ slotOf.set(member.id, member.order ? declared.indexOf(member.order.value) + 1 : lowest);
99546
99793
  }
99547
99794
  const ordered = new Set(model.elements.filter((element) => element.order).map((e) => e.id));
99548
99795
  for (let pass = 0; pass < model.elements.length; pass++) {
@@ -99561,13 +99808,15 @@ function readingSlots(model, view, ingressExternal) {
99561
99808
  }
99562
99809
  return slotOf;
99563
99810
  }
99811
+ function elkEnds(flow) {
99812
+ return laidOutReversed(flow) ? { sources: [flow.to], targets: [flow.from] } : { sources: [flow.from], targets: [flow.to] };
99813
+ }
99564
99814
  function elkFlowEdge(flow, ctx, labelWrap) {
99565
99815
  const { compact, numbered, fonts, businessObjectName } = ctx;
99566
99816
  if (numbered)
99567
99817
  return {
99568
99818
  id: flow.id,
99569
- sources: [flow.from],
99570
- targets: [flow.to],
99819
+ ...elkEnds(flow),
99571
99820
  labels: [
99572
99821
  {
99573
99822
  text: String(parseInt(flow.id.slice(1), 10)),
@@ -99592,13 +99841,35 @@ function elkFlowEdge(flow, ctx, labelWrap) {
99592
99841
  });
99593
99842
  return {
99594
99843
  id: flow.id,
99595
- sources: [flow.from],
99596
- targets: [flow.to],
99844
+ ...elkEnds(flow),
99597
99845
  labels: text || chips.length ? [{ text, ...labelBox }] : []
99598
99846
  };
99599
99847
  }
99600
- function applyDeclaredPorts(graph, model) {
99601
- const pinned = model.flows.filter((flow) => flow.fromSide || flow.toSide);
99848
+ function laidOutReversed(flow) {
99849
+ return flow.fromRole?.value === "consumer" || flow.toRole?.value === "producer";
99850
+ }
99851
+ function roleCap(role) {
99852
+ if (!role) return null;
99853
+ return role.value === "producer" ? "left" : "right";
99854
+ }
99855
+ function hubFlowSides(model, view) {
99856
+ const derived = /* @__PURE__ */ new Map();
99857
+ const hubKinds = new Set(view.hubKinds ?? []);
99858
+ if (!hubKinds.size) return derived;
99859
+ const hubs = new Set(
99860
+ indexElementsById(model.elements).filter(([, element]) => hubKinds.has(element.kind)).map(([id]) => id)
99861
+ );
99862
+ if (!hubs.size) return derived;
99863
+ for (const flow of model.flows) {
99864
+ const sides = {};
99865
+ if (hubs.has(flow.to) && !flow.toSide) sides.to = roleCap(flow.fromRole) ?? "left";
99866
+ if (hubs.has(flow.from) && !flow.fromSide) sides.from = roleCap(flow.toRole) ?? "right";
99867
+ if (sides.to || sides.from) derived.set(flow.id, sides);
99868
+ }
99869
+ return derived;
99870
+ }
99871
+ function applyDeclaredPorts(graph, model, derived) {
99872
+ const pinned = model.flows.filter((flow) => flow.fromSide || flow.toSide || derived.has(flow.id));
99602
99873
  if (!pinned.length) return;
99603
99874
  const elkById = /* @__PURE__ */ new Map();
99604
99875
  const register = (node) => {
@@ -99609,14 +99880,15 @@ function applyDeclaredPorts(graph, model) {
99609
99880
  for (const flow of pinned) {
99610
99881
  const elkEdge = (graph.edges ?? []).find((edge) => edge.id === flow.id);
99611
99882
  if (!elkEdge) continue;
99883
+ const hub = derived.get(flow.id);
99612
99884
  for (const [role, declared, nodeId] of [
99613
- ["out", flow.fromSide, flow.from],
99614
- ["in", flow.toSide, flow.to]
99885
+ ["out", flow.fromSide ?? sideRequest(hub?.from), flow.from],
99886
+ ["in", flow.toSide ?? sideRequest(hub?.to), flow.to]
99615
99887
  ]) {
99616
99888
  if (!declared) continue;
99617
99889
  const elkNode = elkById.get(nodeId);
99618
99890
  if (!elkNode) continue;
99619
- const portId = `${flow.id}#${role}`;
99891
+ const portId = `${flow.id}#${role === "out" !== laidOutReversed(flow) ? "out" : "in"}`;
99620
99892
  elkNode.ports = [
99621
99893
  ...elkNode.ports ?? [],
99622
99894
  {
@@ -99627,7 +99899,7 @@ function applyDeclaredPorts(graph, model) {
99627
99899
  }
99628
99900
  ];
99629
99901
  elkNode.layoutOptions = { ...elkNode.layoutOptions, "elk.portConstraints": "FIXED_SIDE" };
99630
- if (role === "out") elkEdge.sources = [portId];
99902
+ if (role === "out" !== laidOutReversed(flow)) elkEdge.sources = [portId];
99631
99903
  else elkEdge.targets = [portId];
99632
99904
  }
99633
99905
  }
@@ -99672,6 +99944,7 @@ function attachSideDiagnostics(scene, model) {
99672
99944
  }
99673
99945
  function buildElkGraph(ctx, direction, options) {
99674
99946
  const { model, view, ingressExternal, slotOf, compact, numbered, fonts } = ctx;
99947
+ const glyphKinds = new Set(view.glyphKinds ?? []);
99675
99948
  const graph = {
99676
99949
  id: "root",
99677
99950
  layoutOptions: {
@@ -99688,7 +99961,7 @@ function buildElkGraph(ctx, direction, options) {
99688
99961
  "elk.edgeRouting": "ORTHOGONAL",
99689
99962
  "elk.partitioning.activate": "true",
99690
99963
  "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
99691
- "elk.layered.compaction.postCompaction.strategy": "EDGE_LENGTH",
99964
+ "elk.layered.compaction.postCompaction.strategy": options?.postCompaction ?? "EDGE_LENGTH",
99692
99965
  "elk.layered.feedbackEdges": "true",
99693
99966
  "elk.layered.thoroughness": "30",
99694
99967
  "elk.separateConnectedComponents": "false",
@@ -99707,25 +99980,38 @@ function buildElkGraph(ctx, direction, options) {
99707
99980
  "elk.spacing.edgeNode": "18",
99708
99981
  "elk.layered.thoroughness": "80",
99709
99982
  "elk.layered.nodePlacement.favorStraightEdges": "true"
99710
- } : {}
99983
+ } : {},
99984
+ // Spread last, after every spacing default above, because overriding them
99985
+ // is the whole point — a density candidate that lands before them is
99986
+ // silently shadowed and lays out exactly like the layout it was meant to
99987
+ // beat.
99988
+ ...options?.dense ? {
99989
+ "elk.layered.spacing.nodeNodeBetweenLayers": "14",
99990
+ "elk.spacing.nodeNode": "10",
99991
+ "elk.spacing.edgeEdge": "8",
99992
+ "elk.spacing.edgeNode": "9"
99993
+ } : {},
99994
+ ...options?.placement ? { "elk.layered.nodePlacement.strategy": options.placement } : {}
99711
99995
  },
99712
99996
  children: model.elements.map((element, index) => {
99713
- const elkNode = toElkNode2(element, compact, fonts, true);
99997
+ const elkNode = toElkNode2(element, { compact, fonts, glyphKinds }, true);
99714
99998
  const band = elkPartitionOf(element, index, view, ingressExternal);
99715
99999
  const slot = slotOf.get(element.id);
99716
100000
  elkNode.layoutOptions = {
99717
100001
  ...elkNode.layoutOptions,
99718
100002
  // Without an `order:` anywhere the band is emitted as it always was, so
99719
100003
  // the drawing is byte-identical to one from before the hint existed.
99720
- "elk.partitioning.partition": String(
99721
- slot === void 0 ? band : band * SLOT_SCALE + slot
99722
- )
100004
+ "elk.partitioning.partition": String(slot === void 0 ? band : band * SLOT_SCALE + slot)
99723
100005
  };
99724
100006
  return elkNode;
99725
100007
  }),
99726
100008
  edges: model.flows.map((flow) => elkFlowEdge(flow, ctx, options?.labelWrap))
99727
100009
  };
99728
- applyDeclaredPorts(graph, model);
100010
+ applyDeclaredPorts(
100011
+ graph,
100012
+ model,
100013
+ options?.hubPorts === false ? /* @__PURE__ */ new Map() : hubFlowSides(model, view)
100014
+ );
99729
100015
  return graph;
99730
100016
  }
99731
100017
  function recordRepairs(scene, routesBefore) {
@@ -99738,8 +100024,127 @@ function recordRepairs(scene, routesBefore) {
99738
100024
  if (moved) edge.repairedFrom = original;
99739
100025
  }
99740
100026
  }
100027
+ function segmentsCross2(a1, a2, b1, b2) {
100028
+ const aH = Math.abs(a1.y - a2.y) < 0.5;
100029
+ const bH = Math.abs(b1.y - b2.y) < 0.5;
100030
+ if (aH === bH) return false;
100031
+ const [h1, h2, v1, v2] = aH ? [a1, a2, b1, b2] : [b1, b2, a1, a2];
100032
+ const hy = h1.y;
100033
+ const vx = v1.x;
100034
+ return vx > Math.min(h1.x, h2.x) + 0.5 && vx < Math.max(h1.x, h2.x) - 0.5 && hy > Math.min(v1.y, v2.y) + 0.5 && hy < Math.max(v1.y, v2.y) - 0.5;
100035
+ }
100036
+ function shortJogs(edges) {
100037
+ let total = 0;
100038
+ for (const edge of edges)
100039
+ for (let i = 1; i + 2 < edge.pts.length; i++) {
100040
+ const run = Math.abs(edge.pts[i + 1].x - edge.pts[i].x) + Math.abs(edge.pts[i + 1].y - edge.pts[i].y);
100041
+ if (run > 0 && run <= 20) total++;
100042
+ }
100043
+ return total;
100044
+ }
100045
+ function crossingsAround(scene, subject) {
100046
+ let total = 0;
100047
+ const subjectIds = new Set(subject.map((edge) => edge.id));
100048
+ for (const edge of subject)
100049
+ for (const other of scene.edges) {
100050
+ if (other.id === edge.id) continue;
100051
+ if (subjectIds.has(other.id) && other.id < edge.id) continue;
100052
+ for (let i = 1; i < edge.pts.length; i++)
100053
+ for (let j = 1; j < other.pts.length; j++)
100054
+ if (segmentsCross2(edge.pts[i - 1], edge.pts[i], other.pts[j - 1], other.pts[j])) total++;
100055
+ }
100056
+ return total;
100057
+ }
100058
+ function leafBoxesOf(scene) {
100059
+ return scene.nodes.filter((node) => !node.container);
100060
+ }
100061
+ function crossesLeaf(edge, leaves, attached) {
100062
+ for (let i = 1; i < edge.pts.length; i++) {
100063
+ const a = edge.pts[i - 1];
100064
+ const b = edge.pts[i];
100065
+ const horizontal = Math.abs(a.y - b.y) < 0.5;
100066
+ const vertical = Math.abs(a.x - b.x) < 0.5;
100067
+ if (!horizontal && !vertical) continue;
100068
+ const at = horizontal ? a.y : a.x;
100069
+ const lo = horizontal ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
100070
+ const hi = horizontal ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
100071
+ for (const node of leaves) {
100072
+ if (attached.has(node.id)) continue;
100073
+ const struck = horizontal ? at > node.y + 1 && at < node.y + node.height - 1 && lo < node.x + node.width - 1 && hi > node.x + 1 : at > node.x + 1 && at < node.x + node.width - 1 && lo < node.y + node.height - 1 && hi > node.y + 1;
100074
+ if (struck) return true;
100075
+ }
100076
+ }
100077
+ return false;
100078
+ }
100079
+ function snapLanes(scene, laneOf, axis) {
100080
+ if (!laneOf.size) return;
100081
+ const leaves = leafBoxesOf(scene);
100082
+ const tilted = (edges) => edges.reduce(
100083
+ (total, edge) => total + edge.pts.filter(
100084
+ (point, i) => i > 0 && Math.abs(point.x - edge.pts[i - 1].x) >= 0.5 && Math.abs(point.y - edge.pts[i - 1].y) >= 0.5
100085
+ ).length,
100086
+ 0
100087
+ );
100088
+ const touching = /* @__PURE__ */ new Map();
100089
+ const attachedTo = /* @__PURE__ */ new Map();
100090
+ for (const edge of scene.edges) {
100091
+ const ends = /* @__PURE__ */ new Set();
100092
+ for (const point of [edge.pts[0], edge.pts[edge.pts.length - 1]]) {
100093
+ if (!point) continue;
100094
+ for (const node of scene.nodes)
100095
+ if (point.x >= node.x - 1 && point.x <= node.x + node.width + 1 && point.y >= node.y - 1 && point.y <= node.y + node.height + 1) {
100096
+ ends.add(node.id);
100097
+ if (!touching.has(node.id)) touching.set(node.id, []);
100098
+ touching.get(node.id).push(edge);
100099
+ }
100100
+ }
100101
+ attachedTo.set(edge.id, ends);
100102
+ }
100103
+ const byLane = /* @__PURE__ */ new Map();
100104
+ for (const node of scene.nodes) {
100105
+ const lane = laneOf.get(node.id);
100106
+ if (lane === void 0) continue;
100107
+ if (!byLane.has(lane)) byLane.set(lane, []);
100108
+ byLane.get(lane).push(node);
100109
+ }
100110
+ for (const members of [...byLane.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1])) {
100111
+ if (members.length < 2) continue;
100112
+ const target = Math.max(...members.map((node) => node[axis]));
100113
+ for (const node of members) {
100114
+ const delta = target - node[axis];
100115
+ if (!delta) continue;
100116
+ const box = { x: node.x, y: node.y, w: node.width, h: node.height };
100117
+ const edges = touching.get(node.id) ?? [];
100118
+ const before = edges.map((edge) => edge.pts.map((point) => ({ ...point })));
100119
+ const crossingsBefore = crossingsAround(scene, edges);
100120
+ const jogsBefore = shortJogs(edges);
100121
+ const tiltsBefore = tilted(edges);
100122
+ node[axis] = target;
100123
+ for (const edge of edges)
100124
+ for (const point of edge.pts)
100125
+ if (point.x >= box.x - 1 && point.x <= box.x + box.w + 1 && point.y >= box.y - 1 && point.y <= box.y + box.h + 1)
100126
+ point[axis] += delta;
100127
+ const broke = crossingsAround(scene, edges) > crossingsBefore || shortJogs(edges) > jogsBefore || tilted(edges) > tiltsBefore || edges.some((edge) => crossesLeaf(edge, leaves, attachedTo.get(edge.id) ?? /* @__PURE__ */ new Set())) || // Containers included: a lane member is top-level, so it has no
100128
+ // legitimate container ancestor and may not land on one.
100129
+ scene.nodes.some(
100130
+ (other) => other !== node && !node.container && other.x < node.x + node.width && node.x < other.x + other.width && other.y < node.y + node.height && node.y < other.y + other.height
100131
+ );
100132
+ if (broke) {
100133
+ node[axis] = target - delta;
100134
+ edges.forEach((edge, i) => {
100135
+ edge.pts = before[i];
100136
+ });
100137
+ }
100138
+ }
100139
+ }
100140
+ const maxX = Math.max(...scene.nodes.map((node) => node.x + node.width));
100141
+ const maxY = Math.max(...scene.nodes.map((node) => node.y + node.height));
100142
+ scene.width = Math.max(scene.width, Math.ceil(maxX) + 10);
100143
+ scene.height = Math.max(scene.height, Math.ceil(maxY) + 10);
100144
+ }
99741
100145
  function runGeometryPasses(scene, model, options) {
99742
100146
  const { numbered, sideways } = options;
100147
+ if (options.laneOf) snapLanes(scene, options.laneOf, sideways ? "y" : "x");
99743
100148
  const titleBoxes = titleBoxesOf(scene, model);
99744
100149
  if (sideways) transpose(scene, titleBoxes);
99745
100150
  rerouteDetours(scene, model, numbered, titleBoxes);
@@ -99758,6 +100163,95 @@ function runGeometryPasses(scene, model, options) {
99758
100163
  clearSideHugs(scene, settledTitles);
99759
100164
  anchorFlowLabels(scene, settledTitles);
99760
100165
  swapCrossingSiblingSeats(scene);
100166
+ fitCanvas(scene);
100167
+ }
100168
+ function laneAssignment(model, view) {
100169
+ const laneOf = /* @__PURE__ */ new Map();
100170
+ const laneKinds = new Set(view.laneKinds ?? []);
100171
+ if (!laneKinds.size || model.style.compact) return laneOf;
100172
+ const rootOf = /* @__PURE__ */ new Map();
100173
+ const walk = (element, root) => {
100174
+ rootOf.set(element.id, root);
100175
+ for (const child of element.children ?? []) walk(child, root);
100176
+ };
100177
+ for (const element of model.elements) walk(element, element.id);
100178
+ const adjacency = /* @__PURE__ */ new Map();
100179
+ for (const flow of model.flows) {
100180
+ const from = rootOf.get(flow.from);
100181
+ const to = rootOf.get(flow.to);
100182
+ if (!from || !to || from === to) continue;
100183
+ if (!adjacency.has(from)) adjacency.set(from, /* @__PURE__ */ new Set());
100184
+ adjacency.get(from).add(to);
100185
+ }
100186
+ const reaches = (from, to) => {
100187
+ const seen = /* @__PURE__ */ new Set([from]);
100188
+ const queue = [from];
100189
+ while (queue.length) {
100190
+ for (const next of adjacency.get(queue.pop()) ?? []) {
100191
+ if (next === to) return true;
100192
+ if (!seen.has(next)) {
100193
+ seen.add(next);
100194
+ queue.push(next);
100195
+ }
100196
+ }
100197
+ }
100198
+ return false;
100199
+ };
100200
+ const byKind = /* @__PURE__ */ new Map();
100201
+ for (const element of model.elements) {
100202
+ if (!laneKinds.has(element.kind)) continue;
100203
+ if ((element.children ?? []).length) continue;
100204
+ if (!byKind.has(element.kind)) byKind.set(element.kind, []);
100205
+ byKind.get(element.kind).push(element);
100206
+ }
100207
+ const pinned = /* @__PURE__ */ new Set();
100208
+ for (const flow of model.flows) {
100209
+ if (!flow.fromSide && !flow.toSide) continue;
100210
+ pinned.add(rootOf.get(flow.from) ?? flow.from);
100211
+ pinned.add(rootOf.get(flow.to) ?? flow.to);
100212
+ }
100213
+ let laneId = 0;
100214
+ for (const members of byKind.values()) {
100215
+ if (members.length < 2) continue;
100216
+ if (members.some((member) => pinned.has(member.id))) continue;
100217
+ if (members.some((member) => member.order)) continue;
100218
+ if (members.some((a) => members.some((b) => a !== b && reaches(a.id, b.id)))) continue;
100219
+ laneId++;
100220
+ for (const member of members) laneOf.set(member.id, laneId);
100221
+ }
100222
+ return laneOf;
100223
+ }
100224
+ async function withHubPortFallback(elk, makeGraph, direction, options) {
100225
+ const run = async (spec) => await elk.layout(makeGraph(direction, spec));
100226
+ const rungs = options?.hubPorts === false ? [] : [
100227
+ { ...options, postCompaction: "EDGE_LENGTH_CONSTRAINT_LOCKING" },
100228
+ { ...options, postCompaction: "NONE" },
100229
+ { ...options, hubPorts: false }
100230
+ ];
100231
+ try {
100232
+ return { result: await run(options), options };
100233
+ } catch (error) {
100234
+ for (const [index, rung] of rungs.entries()) {
100235
+ try {
100236
+ return { result: await run(rung), options: rung };
100237
+ } catch (rungError) {
100238
+ if (index === rungs.length - 1) throw rungError;
100239
+ }
100240
+ }
100241
+ throw error;
100242
+ }
100243
+ }
100244
+ function markDeclaredTerminals(edges, model, view, hubPorts) {
100245
+ const pinnedFlows = new Map(
100246
+ model.flows.filter((flow) => flow.fromSide || flow.toSide).map((flow) => [flow.id, { start: !!flow.fromSide, end: !!flow.toSide }])
100247
+ );
100248
+ const hubSides = hubPorts ? hubFlowSides(model, view) : /* @__PURE__ */ new Map();
100249
+ for (const edge of edges) {
100250
+ const pinned = pinnedFlows.get(edge.id);
100251
+ if (pinned) edge.pinned = { ...pinned };
100252
+ const hub = hubSides.get(edge.id);
100253
+ if (hub) edge.hubSided = { start: !!hub.from, end: !!hub.to };
100254
+ }
99761
100255
  }
99762
100256
  async function layout(model, view) {
99763
100257
  const elk = await getElk();
@@ -99771,10 +100265,6 @@ async function layout(model, view) {
99771
100265
  scale: fontScale
99772
100266
  } = fontSizes(model.style.font.size);
99773
100267
  const disposition = model.style.disposition;
99774
- const ASPECT_TARGETS = {
99775
- slide: 16 / 9,
99776
- page: 0.71
99777
- };
99778
100268
  const aspectTarget = ASPECT_TARGETS[disposition];
99779
100269
  const ingressExternalElements = view.partitionByOrder ? computeIngressExternalElements(model) : /* @__PURE__ */ new Set();
99780
100270
  const graphContext = {
@@ -99794,7 +100284,7 @@ async function layout(model, view) {
99794
100284
  };
99795
100285
  const makeGraph = (direction, options) => buildElkGraph(graphContext, direction, options);
99796
100286
  const kindOf = new Map(indexElementsById(model.elements));
99797
- const sceneFromResult = (result2, layoutMs2) => {
100287
+ const sceneFromResult = (result2, layoutMs2, laidOutWith, lanes = true) => {
99798
100288
  const origins = {
99799
100289
  root: { x: 0, y: 0 }
99800
100290
  };
@@ -99802,13 +100292,9 @@ async function layout(model, view) {
99802
100292
  const nodes = walkedNodes.map((walked) => walked.node);
99803
100293
  for (const walked of walkedNodes) origins[walked.id] = { x: walked.x, y: walked.y };
99804
100294
  const edges = collectSceneEdges(result2, origins, numbered, edgeFontSize);
99805
- const pinnedFlows = new Map(
99806
- model.flows.filter((flow) => flow.fromSide || flow.toSide).map((flow) => [flow.id, { start: !!flow.fromSide, end: !!flow.toSide }])
99807
- );
99808
- for (const edge of edges) {
99809
- const pinned = pinnedFlows.get(edge.id);
99810
- if (pinned) edge.pinned = { ...pinned };
99811
- }
100295
+ const reversed = new Set(model.flows.filter(laidOutReversed).map((flow) => flow.id));
100296
+ for (const edge of edges) if (reversed.has(edge.id)) edge.pts.reverse();
100297
+ markDeclaredTerminals(edges, model, view, laidOutWith?.hubPorts !== false);
99812
100298
  const scene = {
99813
100299
  width: Math.ceil(result2.width),
99814
100300
  height: Math.ceil(result2.height),
@@ -99818,11 +100304,14 @@ async function layout(model, view) {
99818
100304
  };
99819
100305
  runGeometryPasses(scene, model, {
99820
100306
  numbered,
99821
- sideways: disposition === "page" || disposition === "tall"
100307
+ sideways: disposition === "page" || disposition === "tall",
100308
+ laneOf: lanes ? laneOf : void 0
99822
100309
  });
99823
100310
  return scene;
99824
100311
  };
100312
+ const laneOf = laneAssignment(model, view);
99825
100313
  const startTime = Date.now();
100314
+ const layoutGraph = (direction, options) => withHubPortFallback(elk, makeGraph, direction, options);
99826
100315
  let result;
99827
100316
  let winnerDirection;
99828
100317
  let winnerOptions;
@@ -99833,9 +100322,10 @@ async function layout(model, view) {
99833
100322
  { direction: "RIGHT", options: { labelWrap: 14, tight: true } },
99834
100323
  { direction: "RIGHT", options: { labelWrap: 14, tight: true, minLayers: true } }
99835
100324
  ] : [{ direction: "DOWN" }, { direction: "DOWN", options: { labelWrap: 16 } }];
99836
- const candidates = await Promise.all(
99837
- graphSpecs.map((spec) => elk.layout(makeGraph(spec.direction, spec.options)))
100325
+ const laidOutSpecs = await Promise.all(
100326
+ graphSpecs.map((spec) => layoutGraph(spec.direction, spec.options))
99838
100327
  );
100328
+ const candidates = laidOutSpecs.map((laidOut) => laidOut.result);
99839
100329
  const preferWide = disposition === "slide";
99840
100330
  const orientedLayouts = candidates.map((layoutResult, index) => ({ layoutResult, index })).filter(
99841
100331
  ({ layoutResult }) => preferWide ? layoutResult.width >= layoutResult.height : layoutResult.height >= layoutResult.width
@@ -99848,7 +100338,7 @@ async function layout(model, view) {
99848
100338
  );
99849
100339
  result = winner.layoutResult;
99850
100340
  winnerDirection = graphSpecs[winner.index].direction;
99851
- winnerOptions = graphSpecs[winner.index].options;
100341
+ winnerOptions = laidOutSpecs[winner.index].options;
99852
100342
  if (disposition === "slide") {
99853
100343
  const folded = await foldedLayout(model, view, elk);
99854
100344
  if (folded && fitScore(result) >= fitScore(folded) * 1.1) {
@@ -99862,20 +100352,32 @@ async function layout(model, view) {
99862
100352
  }
99863
100353
  } else {
99864
100354
  winnerDirection = disposition === "tall" ? "DOWN" : "RIGHT";
99865
- winnerOptions = void 0;
99866
- result = await elk.layout(makeGraph(winnerDirection));
100355
+ ({ result, options: winnerOptions } = await layoutGraph(winnerDirection));
99867
100356
  }
99868
100357
  const layoutMs = Date.now() - startTime;
99869
- const base = sceneFromResult(result, layoutMs);
99870
- const skipPortPass = !!globalThis.process?.env?.CAIRN_NO_PORT_PASS;
99871
- if (skipPortPass) return base;
100358
+ let base = sceneFromResult(result, layoutMs, winnerOptions);
99872
100359
  const layoutProfile = (candidate) => {
99873
100360
  const everyEdge = new Set(candidate.edges.map((edge) => edge.id));
99874
100361
  const profile = inspect(candidate, titleBoxesOf(candidate, model)).local(everyEdge, /* @__PURE__ */ new Map());
99875
100362
  for (const [key, tier] of selectionExtras(candidate, model)) profile.set(key, tier);
99876
100363
  return profile;
99877
100364
  };
100365
+ if (!aspectTarget && nodeCoverage(base) < DENSE_ENOUGH) {
100366
+ const denser = await denserLayout(base, {
100367
+ layout: (spec) => elk.layout(makeGraph(winnerDirection, spec)),
100368
+ // Its specs carry the hub ports (none of them sets `hubPorts: false`), and
100369
+ // a candidate elk refuses is dropped rather than retried without them.
100370
+ toScene: (laidOut) => sceneFromResult(laidOut, Date.now() - startTime),
100371
+ profile: layoutProfile
100372
+ });
100373
+ if (denser) {
100374
+ base = denser.scene;
100375
+ winnerOptions = denser.options;
100376
+ }
100377
+ }
99878
100378
  const baseProfile = layoutProfile(base);
100379
+ const skipPortPass = !!globalThis.process?.env?.CAIRN_NO_PORT_PASS;
100380
+ if (skipPortPass) return base;
99879
100381
  let current = base;
99880
100382
  let best = null;
99881
100383
  for (let round = 0; round < PORT_PASS_ROUNDS; round++) {
@@ -99886,7 +100388,7 @@ async function layout(model, view) {
99886
100388
  const constrained = makeGraph(winnerDirection, winnerOptions);
99887
100389
  constrainPorts(constrained, current, flagged, model);
99888
100390
  const reresult = await elk.layout(constrained);
99889
- candidate = sceneFromResult(reresult, Date.now() - startTime);
100391
+ candidate = sceneFromResult(reresult, Date.now() - startTime, winnerOptions);
99890
100392
  } catch {
99891
100393
  break;
99892
100394
  }
@@ -99898,7 +100400,7 @@ async function layout(model, view) {
99898
100400
  }
99899
100401
  return best?.scene ?? base;
99900
100402
  }
99901
- var orderOption, semiInteractiveOption, PORT_PASS_ROUNDS, centerOf, INGRESS_PARTITION, EGRESS_PARTITION, COMPACT_WRAP, SLOT_SCALE, SIDE_TO_ELK;
100403
+ var orderOption, semiInteractiveOption, PORT_PASS_ROUNDS, centerOf, DENSE_ENOUGH, DENSITY_GAIN, INGRESS_PARTITION, DEFAULT_INGRESS_KINDS, EGRESS_PARTITION, COMPACT_WRAP, SLOT_SCALE, SIDE_TO_ELK, sideRequest, ASPECT_TARGETS;
99902
100404
  var init_scene_layout = __esm({
99903
100405
  "src/scene-layout.ts"() {
99904
100406
  "use strict";
@@ -99916,7 +100418,10 @@ var init_scene_layout = __esm({
99916
100418
  semiInteractiveOption = (children) => children.some((child) => child.order) ? { "elk.layered.crossingMinimization.semiInteractive": "true" } : {};
99917
100419
  PORT_PASS_ROUNDS = 2;
99918
100420
  centerOf = (n) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 });
100421
+ DENSE_ENOUGH = 0.6;
100422
+ DENSITY_GAIN = 0.95;
99919
100423
  INGRESS_PARTITION = -1;
100424
+ DEFAULT_INGRESS_KINDS = ["actor", "actor-group"];
99920
100425
  EGRESS_PARTITION = 900;
99921
100426
  COMPACT_WRAP = 10;
99922
100427
  SLOT_SCALE = 1e3;
@@ -99926,6 +100431,11 @@ var init_scene_layout = __esm({
99926
100431
  top: "NORTH",
99927
100432
  bottom: "SOUTH"
99928
100433
  };
100434
+ sideRequest = (side) => side ? { value: side } : void 0;
100435
+ ASPECT_TARGETS = {
100436
+ slide: 16 / 9,
100437
+ page: 0.71
100438
+ };
99929
100439
  }
99930
100440
  });
99931
100441
 
@@ -99976,16 +100486,62 @@ var init_localization = __esm({
99976
100486
  });
99977
100487
 
99978
100488
  // src/xml-escape.ts
99979
- var esc, escAttr;
100489
+ var esc, escAttr, escComment;
99980
100490
  var init_xml_escape = __esm({
99981
100491
  "src/xml-escape.ts"() {
99982
100492
  "use strict";
99983
100493
  esc = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
99984
100494
  escAttr = (text) => esc(text).replace(/"/g, "&quot;");
100495
+ escComment = (text) => text.replace(/-{2,}/g, (run) => [...run].join(" ")).replace(/-$/, "- ");
100496
+ }
100497
+ });
100498
+
100499
+ // src/logo-attribution.ts
100500
+ function logoAttributionComment(drawn) {
100501
+ const licensed = [...drawn].sort().map((slug) => ({ slug, logo: LOGOS[slug] })).filter((entry) => entry.logo?.license);
100502
+ if (licensed.length === 0) return "";
100503
+ const lines = [...PREAMBLE];
100504
+ for (const { slug, logo } of licensed) {
100505
+ lines.push("", `${logo.title} (${slug}) \u2014 ${logo.license}`);
100506
+ if (logo.copyright) lines.push(` ${logo.copyright}`);
100507
+ if (logo.source) lines.push(` artwork: ${logo.source}`);
100508
+ if (logo.licenseUrl) lines.push(` licence: ${logo.licenseUrl}`);
100509
+ }
100510
+ return `<!--
100511
+ ${lines.map((line) => escComment(` ${line}`).trimEnd()).join("\n")}
100512
+ -->
100513
+ `;
100514
+ }
100515
+ var PREAMBLE;
100516
+ var init_logo_attribution = __esm({
100517
+ "src/logo-attribution.ts"() {
100518
+ "use strict";
100519
+ init_logos();
100520
+ init_xml_escape();
100521
+ PREAMBLE = [
100522
+ "This diagram contains third-party artwork under its own licence.",
100523
+ "Keep this notice with the file \u2014 it is the attribution those licences ask for.",
100524
+ "",
100525
+ "Every mark below is modified from the original: simple-icons redrew it as a",
100526
+ "single monochrome path, and cairn paints that path in the diagram's colours."
100527
+ ];
99985
100528
  }
99986
100529
  });
99987
100530
 
99988
100531
  // src/svg-render.ts
100532
+ function bandLines(text, from, right, fontSize) {
100533
+ const chars = Math.floor((right - from) / (fontSize * RENDER_CHAR_WIDTH));
100534
+ return bandWrap(text, Math.max(8, chars));
100535
+ }
100536
+ function bandWrap(text, maxChars) {
100537
+ return wrapText(text, maxChars).split("\n").flatMap((line) => {
100538
+ if (line.length <= maxChars) return [line];
100539
+ const parts = [];
100540
+ for (let index = 0; index < line.length; index += maxChars)
100541
+ parts.push(line.slice(index, index + maxChars));
100542
+ return parts;
100543
+ });
100544
+ }
99989
100545
  function assignSourceHues(model, hues) {
99990
100546
  const sourceHue = /* @__PURE__ */ new Map();
99991
100547
  for (const flow of model.flows) {
@@ -99996,7 +100552,7 @@ function assignSourceHues(model, hues) {
99996
100552
  }
99997
100553
  function collectElementStyles(elements) {
99998
100554
  return elements.flatMap((element) => [
99999
- { id: element.id, style: element.style, attrValue: element.attr?.value },
100555
+ { id: element.id, style: element.style, logo: element.logo },
100000
100556
  ...collectElementStyles(element.children)
100001
100557
  ]);
100002
100558
  }
@@ -100038,19 +100594,39 @@ function auditRouteRepairs(deps) {
100038
100594
  for (let tier = 0; tier < 5; tier++) if (a[tier] !== b[tier]) return a[tier] < b[tier];
100039
100595
  return false;
100040
100596
  };
100597
+ const flowOfBreach = (key) => {
100598
+ if (key.startsWith("adrift:"))
100599
+ return labels[Number.parseInt(key.slice("adrift:".length), 10)]?.flowId ?? null;
100600
+ const body = key.slice(key.indexOf(":") + 1);
100601
+ const cut = body.search(/[~:]/);
100602
+ return cut < 0 ? body : body.slice(0, cut);
100603
+ };
100041
100604
  const withRepair = stateHarm();
100042
100605
  const breachesWith = breaches();
100043
100606
  const repairedRoutes = repaired.map((edge) => edge.pts);
100044
- for (const edge of repaired) edge.pts = edge.repairedFrom;
100045
- resettle();
100046
- const withoutRepair = stateHarm();
100047
- const breachesWithout = breaches();
100048
- const breaksAPromise = [...breachesWith].some((key) => !breachesWithout.has(key));
100049
- if (!breaksAPromise && !lessDamaged(withoutRepair, withRepair)) {
100607
+ const replacedRoutes = repaired.map((edge) => edge.repairedFrom);
100608
+ const applyKeep = (keep2) => {
100050
100609
  repaired.forEach((edge, index) => {
100051
- edge.pts = repairedRoutes[index];
100610
+ edge.pts = keep2[index] ? repairedRoutes[index] : replacedRoutes[index];
100052
100611
  });
100053
100612
  resettle();
100613
+ };
100614
+ applyKeep(repaired.map(() => false));
100615
+ const withoutRepair = stateHarm();
100616
+ const breachesWithout = breaches();
100617
+ const broken = [...breachesWith].filter((key) => !breachesWithout.has(key));
100618
+ if (!broken.length) {
100619
+ if (!lessDamaged(withoutRepair, withRepair)) applyKeep(repaired.map(() => true));
100620
+ for (const edge of repaired) edge.repairedFrom = void 0;
100621
+ return;
100622
+ }
100623
+ const blamed = new Set(broken.map(flowOfBreach).filter((id) => id !== null));
100624
+ const keep = repaired.map((edge) => !blamed.has(edge.id));
100625
+ if (keep.some(Boolean) && keep.some((kept) => !kept)) {
100626
+ applyKeep(keep);
100627
+ const partly = stateHarm();
100628
+ const stillBroken = [...breaches()].some((key) => !breachesWithout.has(key));
100629
+ if (stillBroken || lessDamaged(withoutRepair, partly)) applyKeep(repaired.map(() => false));
100054
100630
  }
100055
100631
  for (const edge of repaired) edge.repairedFrom = void 0;
100056
100632
  }
@@ -100091,13 +100667,66 @@ function settleOneLabel(s, label) {
100091
100667
  label.x = origin.x;
100092
100668
  label.y = origin.y;
100093
100669
  }
100094
- function createNodeRenderers(paint) {
100095
- const { palette, style, annot, nodeFontSize, containerFontSize, resolveStyle, elementAttr } = paint;
100670
+ function glyphSvg(kind, stroke, box) {
100671
+ const glyph = GLYPHS[kind];
100672
+ if (!glyph) return "";
100673
+ const { x, y, scale = 1 } = box;
100674
+ const width = round1(1.3 * scale);
100675
+ return glyph({
100676
+ x: (v) => round1(x + v * scale),
100677
+ y: (v) => round1(y + v * scale),
100678
+ r: (v) => round1(v * scale),
100679
+ line: `fill="none" stroke="${stroke}" stroke-width="${width}"`,
100680
+ stroke
100681
+ });
100682
+ }
100683
+ function logoSvg(mark) {
100684
+ const { logo, resolved, node, stroke, drawn, inset = {} } = mark;
100685
+ if (!logo) return "";
100686
+ const x = round1(node.x + node.width - (inset.right ?? LOGO_BOX.right) - LOGO_BOX.size);
100687
+ const y = round1(node.y + (inset.top ?? LOGO_BOX.top));
100688
+ if (logo.source === "file") {
100689
+ const href = resolved?.get(node.id);
100690
+ if (!href) return "";
100691
+ return `<image x="${x}" y="${y}" width="${LOGO_BOX.size}" height="${LOGO_BOX.size}" href="${escAttr(href)}" preserveAspectRatio="xMidYMid meet"/>
100692
+ `;
100693
+ }
100694
+ const builtin = Object.hasOwn(LOGOS, logo.value) ? LOGOS[logo.value] : void 0;
100695
+ if (!builtin) return "";
100696
+ drawn.add(logo.value);
100697
+ const scale = Math.round(LOGO_BOX.size / 24 * 1e4) / 1e4;
100698
+ return `<g transform="translate(${x} ${y}) scale(${scale})" fill="${stroke}"><title>${esc(builtin.title)}</title><path d="${builtin.d}"/></g>
100699
+ `;
100700
+ }
100701
+ function createNodeLabelHelpers(nodeFontSize) {
100096
100702
  const centeredNodeLabel = (lines, centerX2, topBaseline, fill) => lines.map(
100097
100703
  (line, index) => `<text x="${centerX2}" y="${topBaseline + index * (nodeFontSize + 2)}" font-size="${nodeFontSize}" text-anchor="middle" fill="${fill}">${esc(line)}</text>
100098
100704
  `
100099
100705
  ).join("");
100100
100706
  const centerLinesY = (top, height, lineCount) => top + height / 2 - (lineCount - 1) * (nodeFontSize + 2) / 2 + 4;
100707
+ const glyphLabelCenterX = (node) => node.x + GLYPH_GUTTER + (node.width - GLYPH_GUTTER) / 2;
100708
+ const logoLabelCenterX = (node, hasLogo) => node.x + (node.width - (hasLogo ? LOGO_GUTTER : 0)) / 2;
100709
+ return { centeredNodeLabel, centerLinesY, glyphLabelCenterX, logoLabelCenterX };
100710
+ }
100711
+ function createNodeRenderers(paint) {
100712
+ const {
100713
+ palette,
100714
+ nodeFontSize,
100715
+ containerFontSize,
100716
+ resolveStyle,
100717
+ elementLogo,
100718
+ resolvedLogos,
100719
+ drawnLogos
100720
+ } = paint;
100721
+ const logoFor = (node, stroke, inset) => logoSvg({
100722
+ logo: elementLogo.get(node.id),
100723
+ resolved: resolvedLogos,
100724
+ node,
100725
+ stroke,
100726
+ drawn: drawnLogos,
100727
+ inset
100728
+ });
100729
+ const { centeredNodeLabel, centerLinesY, glyphLabelCenterX, logoLabelCenterX } = createNodeLabelHelpers(nodeFontSize);
100101
100730
  const renderContainerNode = (node) => {
100102
100731
  const nodeStyle = resolveStyle(node.kind, node.id);
100103
100732
  const fill = escAttr(nodeStyle.fill ?? palette.containerFill), stroke = escAttr(nodeStyle.stroke?.color ?? palette.containerStroke), text = escAttr(nodeStyle.text ?? palette.containerLabel);
@@ -100108,12 +100737,7 @@ function createNodeRenderers(paint) {
100108
100737
  svg += `<text x="${node.x + 10}" y="${node.y + 18 + index * 14}" font-size="${containerFontSize}" font-weight="bold" fill="${text}">${esc(line)}</text>
100109
100738
  `;
100110
100739
  });
100111
- const level = node.kind === "trust-zone" ? elementAttr.get(node.id) : void 0;
100112
- if (level) {
100113
- const word = (style.lang === "fr" ? SEC_LEVEL_FR[level] : level) ?? level;
100114
- svg += `<text x="${node.x + node.width - 9}" y="${node.y + node.height - 6}" font-size="${annot.tag}" text-anchor="end" font-weight="bold" fill="${stroke}" letter-spacing="0.5">${esc(word.toUpperCase())}</text>
100115
- `;
100116
- }
100740
+ svg += logoFor(node, stroke);
100117
100741
  return svg;
100118
100742
  };
100119
100743
  const renderActor = (node, nodeStyle, lines) => {
@@ -100136,9 +100760,12 @@ function createNodeRenderers(paint) {
100136
100760
  <ellipse cx="${node.x + node.width / 2}" cy="${node.y + ry}" rx="${node.width / 2}" ry="${ry}" fill="${fill}" stroke="${stroke}" stroke-width="1.3"/>
100137
100761
  `;
100138
100762
  const centerY2 = node.y + ry + (node.height - ry) / 2 - (lines.length - 1) * (nodeFontSize + 2) / 2 + 4;
100139
- return body + centeredNodeLabel(
100763
+ const logo = logoFor(node, stroke, {
100764
+ top: ry + (node.height - 2 * ry - LOGO_BOX.size) / 2
100765
+ });
100766
+ return body + logo + centeredNodeLabel(
100140
100767
  lines,
100141
- node.x + node.width / 2,
100768
+ logoLabelCenterX(node, logo !== ""),
100142
100769
  centerY2,
100143
100770
  escAttr(nodeStyle.text ?? palette.nodeText)
100144
100771
  );
@@ -100149,50 +100776,38 @@ function createNodeRenderers(paint) {
100149
100776
  const body = `<path d="M ${node.x + rx} ${node.y} h ${node.width - 2 * rx} a ${rx} ${node.height / 2} 0 0 1 0 ${node.height} h ${-(node.width - 2 * rx)} a ${rx} ${node.height / 2} 0 0 1 0 ${-node.height}" fill="${fill}" stroke="${stroke}" stroke-width="1.3"/>
100150
100777
  <ellipse cx="${node.x + rx}" cy="${node.y + node.height / 2}" rx="${rx}" ry="${node.height / 2}" fill="${fill}" stroke="${stroke}" stroke-width="1.3"/>
100151
100778
  `;
100152
- return body + centeredNodeLabel(
100779
+ const logo = logoFor(node, stroke, { right: LOGO_BOX.right + rx });
100780
+ return body + logo + centeredNodeLabel(
100153
100781
  lines,
100154
- node.x + rx + (node.width - rx) / 2,
100782
+ node.x + rx + (node.width - rx - (logo === "" ? 0 : LOGO_GUTTER)) / 2,
100155
100783
  centerLinesY(node.y, node.height, lines.length),
100156
100784
  text
100157
100785
  );
100158
100786
  };
100159
- const renderGateway = (node, nodeStyle, lines) => {
100160
- const centerX2 = node.x + node.width / 2;
100161
- const fill = escAttr(nodeStyle.fill ?? palette.nodeFill), stroke = escAttr(nodeStyle.stroke?.color ?? palette.nodeStroke), text = escAttr(nodeStyle.text ?? palette.nodeText);
100162
- const body = `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="4" fill="${fill}" stroke="${stroke}" stroke-width="1.3"/>
100163
- <path d="M ${round1(node.x + 8)} ${round1(node.y + 8)} L ${round1(node.x + 22)} ${round1(node.y + 8)} Q ${round1(node.x + 24)} ${round1(node.y + 13)} ${round1(node.x + 15)} ${round1(node.y + 20)} Q ${round1(node.x + 6)} ${round1(node.y + 13)} ${round1(node.x + 8)} ${round1(node.y + 8)}" fill="none" stroke="${stroke}" stroke-width="1.3"/>
100164
- `;
100165
- return body + centeredNodeLabel(lines, centerX2 + 10, centerLinesY(node.y, node.height, lines.length), text);
100166
- };
100167
- const renderAuth = (node, nodeStyle, lines) => {
100168
- const centerX2 = node.x + node.width / 2;
100169
- const fill = escAttr(nodeStyle.fill ?? palette.nodeFill), stroke = escAttr(nodeStyle.stroke?.color ?? palette.nodeStroke), text = escAttr(nodeStyle.text ?? palette.nodeText);
100170
- const body = `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="4" fill="${fill}" stroke="${stroke}" stroke-width="1.3"/>
100171
- <rect x="${node.x + 6}" y="${node.y + 6}" width="18" height="14" rx="3" fill="none" stroke="${stroke}" stroke-width="1.3"/>
100172
- <path d="M ${node.x + 10} ${node.y + 9} v -4 a 5 5 0 0 1 10 0 v 4" fill="none" stroke="${stroke}" stroke-width="1.3"/>
100173
- <circle cx="${node.x + 15}" cy="${node.y + 16}" r="2.5" fill="${stroke}"/>
100174
- `;
100175
- return body + centeredNodeLabel(lines, centerX2 + 10, centerLinesY(node.y, node.height, lines.length), text);
100176
- };
100177
- const renderPlainBox = (node, nodeStyle, lines) => {
100787
+ const renderGlyphBox = (node, nodeStyle, lines) => {
100178
100788
  const fill = escAttr(nodeStyle.fill ?? palette.nodeFill), stroke = escAttr(nodeStyle.stroke?.color ?? palette.nodeStroke), text = escAttr(nodeStyle.text ?? palette.nodeText);
100179
100789
  const dash = dashArray(nodeStyle.stroke?.style);
100180
100790
  const body = `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="4" fill="${fill}" stroke="${stroke}" stroke-width="${nodeStyle.stroke?.width ?? 1.3}"${dash ? ` stroke-dasharray="${dash}"` : ""}/>
100181
- `;
100791
+ ` + glyphSvg(node.kind, stroke, {
100792
+ x: node.x + GLYPH_BOX.left,
100793
+ y: node.y + GLYPH_BOX.top
100794
+ }) + "\n";
100182
100795
  return body + centeredNodeLabel(
100183
100796
  lines,
100184
- node.x + node.width / 2,
100797
+ glyphLabelCenterX(node),
100185
100798
  centerLinesY(node.y, node.height, lines.length),
100186
100799
  text
100187
100800
  );
100188
100801
  };
100189
- const renderIdp = (node, nodeStyle, lines) => {
100802
+ const renderPlainBox = (node, nodeStyle, lines) => {
100190
100803
  const fill = escAttr(nodeStyle.fill ?? palette.nodeFill), stroke = escAttr(nodeStyle.stroke?.color ?? palette.nodeStroke), text = escAttr(nodeStyle.text ?? palette.nodeText);
100191
- const body = `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="4" fill="${fill}" stroke="${stroke}" stroke-width="${nodeStyle.stroke?.width ?? 1.3}"/>
100804
+ const dash = dashArray(nodeStyle.stroke?.style);
100805
+ const logo = logoFor(node, stroke);
100806
+ const body = `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="4" fill="${fill}" stroke="${stroke}" stroke-width="${nodeStyle.stroke?.width ?? 1.3}"${dash ? ` stroke-dasharray="${dash}"` : ""}/>
100192
100807
  `;
100193
- return body + centeredNodeLabel(
100808
+ return body + logo + centeredNodeLabel(
100194
100809
  lines,
100195
- node.x + node.width / 2,
100810
+ logoLabelCenterX(node, logo !== ""),
100196
100811
  centerLinesY(node.y, node.height, lines.length),
100197
100812
  text
100198
100813
  );
@@ -100207,14 +100822,8 @@ function createNodeRenderers(paint) {
100207
100822
  return renderDatastore(node, nodeStyle, lines);
100208
100823
  case "queue":
100209
100824
  return renderQueue(node, nodeStyle, lines);
100210
- case "gateway":
100211
- return renderGateway(node, nodeStyle, lines);
100212
- case "auth":
100213
- return renderAuth(node, nodeStyle, lines);
100214
- case "idp":
100215
- return renderIdp(node, nodeStyle, lines);
100216
100825
  default:
100217
- return renderPlainBox(node, nodeStyle, lines);
100826
+ return GLYPHS[node.kind] ? renderGlyphBox(node, nodeStyle, lines) : renderPlainBox(node, nodeStyle, lines);
100218
100827
  }
100219
100828
  };
100220
100829
  return { renderContainerNode, renderLeafNode };
@@ -100281,15 +100890,20 @@ function createLabelSettler(deps) {
100281
100890
  const a = edge.pts[index];
100282
100891
  const b = edge.pts[index + 1];
100283
100892
  const vertical = Math.abs(a.x - b.x) < Math.abs(a.y - b.y);
100284
- const span = vertical ? Math.abs(b.y - a.y) : Math.abs(b.x - a.x);
100285
- const room = Math.max(0, (span - (vertical ? label.height : label.width)) / 2);
100286
- if (room === 0) continue;
100287
- for (const fraction of [-0.25, 0.25, -0.5, 0.5, -0.75, 0.75, -1, 1]) {
100288
- const shift = room * fraction;
100289
- seats.push(
100290
- vertical ? { x: (a.x + b.x) / 2 - label.width / 2, y: (a.y + b.y) / 2 + shift - lead } : { x: (a.x + b.x) / 2 + shift - label.width / 2, y: (a.y + b.y) / 2 - lead }
100291
- );
100292
- }
100893
+ const size = vertical ? label.height : label.width;
100894
+ const start = vertical ? Math.min(a.y, b.y) : Math.min(a.x, b.x);
100895
+ const end = vertical ? Math.max(a.y, b.y) : Math.max(a.x, b.x);
100896
+ const seat = vertical ? (a.y + b.y) / 2 - lead : (a.x + b.x) / 2 - label.width / 2;
100897
+ const back = Math.max(0, seat - start);
100898
+ const ahead = Math.max(0, end - size - seat);
100899
+ if (back === 0 && ahead === 0) continue;
100900
+ for (const fraction of [0.25, 0.5, 0.75, 1])
100901
+ for (const room of [-back, ahead]) {
100902
+ const shift = room * fraction;
100903
+ seats.push(
100904
+ vertical ? { x: (a.x + b.x) / 2 - label.width / 2, y: seat + shift } : { x: seat + shift, y: (a.y + b.y) / 2 - lead }
100905
+ );
100906
+ }
100293
100907
  }
100294
100908
  return seats;
100295
100909
  };
@@ -100336,27 +100950,103 @@ function createLabelSettler(deps) {
100336
100950
  settleLabelPositions
100337
100951
  };
100338
100952
  }
100953
+ function kindKeysSvg(paint, kinds, y, x) {
100954
+ const { scene, palette, legendNames, scaled, resolveStyle } = paint;
100955
+ let svg = "";
100956
+ let keyX = x;
100957
+ let bandY = y;
100958
+ const LINE_H = scaled(12);
100959
+ for (const kind of kinds) {
100960
+ const nodeStyle = resolveStyle(kind, "");
100961
+ if (kind === "actor") {
100962
+ const stroke = nodeStyle.stroke?.color ?? palette.actorStroke;
100963
+ svg += `<circle cx="${keyX + scaled(13)}" cy="${bandY + scaled(5)}" r="${scaled(3)}" fill="none" stroke="${stroke}" stroke-width="1.2"/>
100964
+ `;
100965
+ svg += `<path d="M ${keyX + scaled(8)} ${bandY + scaled(15)} q ${scaled(5)} ${scaled(-7)} ${scaled(10)} 0" fill="none" stroke="${stroke}" stroke-width="1.2"/>
100966
+ `;
100967
+ } else {
100968
+ const dash = dashArray(nodeStyle.stroke?.style);
100969
+ const stroke = nodeStyle.stroke?.color ?? palette.nodeStroke;
100970
+ svg += `<rect x="${keyX}" y="${bandY + 2}" width="${scaled(26)}" height="${scaled(14)}" rx="3" fill="${nodeStyle.fill ?? palette.nodeFill}" stroke="${escAttr(stroke)}"${dash ? ` stroke-dasharray="${dash}"` : ""}/>
100971
+ `;
100972
+ const glyphScale = (scaled(14) - scaled(4)) / GLYPH_BOX.height;
100973
+ svg += glyphSvg(kind, escAttr(stroke), {
100974
+ x: keyX + (scaled(26) - GLYPH_BOX.width * glyphScale) / 2,
100975
+ y: bandY + 2 + (scaled(14) - GLYPH_BOX.height * glyphScale) / 2,
100976
+ scale: glyphScale
100977
+ });
100978
+ }
100979
+ const name = legendNames[kind];
100980
+ const lines = bandLines(name, keyX + scaled(32), bandRightMargin(scene), scaled(10));
100981
+ for (const [row, line] of lines.entries())
100982
+ svg += `<text x="${keyX + scaled(32)}" y="${bandY + scaled(13) + row * LINE_H}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(line)}</text>
100983
+ `;
100984
+ keyX += scaled(40) + Math.ceil(name.length * scaled(10) * RENDER_CHAR_WIDTH) + scaled(24);
100985
+ bandY += (lines.length - 1) * LINE_H;
100986
+ if (lines.length > 1 || keyX > scene.width - 220) {
100987
+ keyX = x;
100988
+ bandY += scaled(22);
100989
+ }
100990
+ }
100991
+ return { svg, bandY: kinds.length ? bandY + scaled(24) : bandY };
100992
+ }
100993
+ function lineStyleKeysSvg(paint, y, x) {
100994
+ const { scene, model, style, palette, legendLineStyles, scaled, defaultEdgeColor, markerName } = paint;
100995
+ const styles = LINE_STYLES2.filter(
100996
+ (lineStyle) => model.flows.some((flow) => lineStyleOf(flow, style) === lineStyle)
100997
+ );
100998
+ if (styles.length < 2) return { svg: "", bandY: y };
100999
+ const maxX = scene.width - 20;
101000
+ let svg = "";
101001
+ let keyX = x;
101002
+ let bandY = y;
101003
+ const LINE_H = scaled(12);
101004
+ for (const [index, lineStyle] of styles.entries()) {
101005
+ const meaning = legendLineStyles[lineStyle];
101006
+ const keyWidth = scaled(40) + Math.ceil(meaning.length * scaled(10) * RENDER_CHAR_WIDTH) + scaled(24);
101007
+ if (keyX > x && keyX + keyWidth > maxX) {
101008
+ keyX = x;
101009
+ bandY += scaled(22);
101010
+ }
101011
+ const dash = dashArray(lineStyle);
101012
+ svg += `<line x1="${keyX}" y1="${bandY + 8}" x2="${keyX + scaled(26)}" y2="${bandY + 8}" stroke="${escAttr(defaultEdgeColor)}" stroke-width="1.3"${dash ? ` stroke-dasharray="${dash}"` : ""} marker-end="url(#${markerName(defaultEdgeColor)})"/>
101013
+ `;
101014
+ const lines = bandLines(meaning, keyX + scaled(40), maxX, scaled(10));
101015
+ for (const [row, line] of lines.entries())
101016
+ svg += `<text x="${keyX + scaled(32)}" y="${bandY + scaled(12) + row * LINE_H}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(line)}</text>
101017
+ `;
101018
+ if (lines.length === 1) {
101019
+ keyX += keyWidth;
101020
+ continue;
101021
+ }
101022
+ bandY += (lines.length - 1) * LINE_H;
101023
+ if (index < styles.length - 1) {
101024
+ bandY += scaled(22);
101025
+ keyX = x;
101026
+ }
101027
+ }
101028
+ return { svg, bandY: bandY + scaled(24) };
101029
+ }
100339
101030
  function createBandRenderers(paint) {
100340
101031
  const {
100341
101032
  scene,
100342
101033
  model,
100343
- view,
100344
101034
  style,
100345
101035
  palette,
100346
101036
  annot,
100347
101037
  ui,
100348
- legendNames,
100349
101038
  legendFlowLabel,
100350
101039
  scaled,
100351
101040
  objectName,
100352
- resolveStyle,
100353
101041
  defaultEdgeColor,
100354
101042
  markerName,
100355
101043
  numbered
100356
101044
  } = paint;
100357
101045
  let bandY = scene.height;
100358
101046
  let bandsSvg = "";
100359
- const contentX = 150;
101047
+ const rightMargin = bandRightMargin(scene);
101048
+ const stackedTitle = rightMargin - BAND_CONTENT_X < MIN_BAND_TEXT;
101049
+ const contentX = stackedTitle ? 20 : BAND_CONTENT_X;
100360
101050
  const chip = (x, y, name) => {
100361
101051
  const width = chipW(name, annot.scale);
100362
101052
  return {
@@ -100371,7 +101061,7 @@ function createBandRenderers(paint) {
100371
101061
  `;
100372
101062
  bandsSvg += `<text x="20" y="${bandY + scaled(32)}" font-size="${scaled(11)}" font-weight="bold" fill="${palette.bandTitle}">${esc(title)}</text>
100373
101063
  `;
100374
- bandY += scaled(20);
101064
+ bandY += scaled(stackedTitle ? 34 : 20);
100375
101065
  };
100376
101066
  const flowChipsSvg = (flow, startX, startY) => {
100377
101067
  if (!flow.objects?.length) return "";
@@ -100403,7 +101093,7 @@ function createBandRenderers(paint) {
100403
101093
  const textW = Math.max(60, colW - BADGE - (chipsW ? chipsW + 6 : 0));
100404
101094
  const maxChars = Math.max(6, Math.floor(textW / (scaled(10) * RENDER_CHAR_WIDTH)));
100405
101095
  const raw = (flow.label ?? "") + (tech ? " " + tech : "");
100406
- const lines = raw.split("\n").flatMap((segment) => wrapText(segment, maxChars).split("\n"));
101096
+ const lines = raw.split("\n").flatMap((segment) => bandWrap(segment, maxChars));
100407
101097
  return { flow, lines };
100408
101098
  });
100409
101099
  const rows = Math.ceil(entries.length / cols);
@@ -100430,59 +101120,50 @@ function createBandRenderers(paint) {
100430
101120
  };
100431
101121
  const renderObjectsBand = () => {
100432
101122
  beginBand(ui.objects);
100433
- for (const bo of model.businessObjects) {
100434
- const chipResult = chip(contentX, bandY + 2, bo.name);
101123
+ const LINE_H = scaled(12);
101124
+ const chipRow = (name, reading) => {
101125
+ const chipResult = chip(contentX, bandY + 2, name);
100435
101126
  bandsSvg += chipResult.svg;
100436
- if (bo.description)
100437
- bandsSvg += `<text x="${contentX + chipResult.width + 10}" y="${bandY + scaled(13)}" font-size="${scaled(10)}" fill="${palette.bandMuted}">\u2014 ${esc(bo.description)}</text>
100438
- `;
100439
- bandY += scaled(24);
100440
- }
100441
- const keyChip = chip(contentX, bandY + 2, ui.businessObject);
100442
- bandsSvg += keyChip.svg;
100443
- bandsSvg += `<text x="${contentX + keyChip.width + 10}" y="${bandY + scaled(13)}" font-size="${scaled(10)}" fill="${palette.bandMuted}">${esc(ui.carriedByFlow)}</text>
101127
+ const textX = contentX + chipResult.width + 10;
101128
+ const lines = reading ? bandLines(reading, textX, rightMargin, scaled(10)) : [];
101129
+ for (const [row, line] of lines.entries())
101130
+ bandsSvg += `<text x="${textX}" y="${bandY + scaled(13) + row * LINE_H}" font-size="${scaled(10)}" fill="${palette.bandMuted}">${esc(line)}</text>
100444
101131
  `;
100445
- bandY += scaled(24) + 6;
101132
+ bandY += scaled(24) + Math.max(0, lines.length - 1) * LINE_H;
101133
+ };
101134
+ for (const bo of model.businessObjects)
101135
+ chipRow(bo.name, bo.description ? `\u2014 ${bo.description}` : "");
101136
+ if (model.flows.some((flow) => flow.objects?.length))
101137
+ chipRow(ui.businessObject, ui.carriedByFlow);
101138
+ bandY += 6;
100446
101139
  };
100447
101140
  const renderLegendBand = () => {
101141
+ const kinds = legendKinds(paint);
101142
+ if (!kinds.length && !model.flows.length && !model.legendNotes.length) return;
100448
101143
  beginBand(ui.legend);
100449
- let lx = contentX;
100450
- const kindsUsed = [...new Set(scene.nodes.map((node) => node.kind))].filter(
100451
- (kind) => legendNames[kind] && (kind !== "actor" || view.actorLegend)
100452
- );
100453
- for (const kind of kindsUsed) {
100454
- const nodeStyle = resolveStyle(kind, "");
100455
- if (kind === "actor") {
100456
- const stroke = nodeStyle.stroke?.color ?? palette.actorStroke;
100457
- bandsSvg += `<circle cx="${lx + scaled(13)}" cy="${bandY + scaled(5)}" r="${scaled(3)}" fill="none" stroke="${stroke}" stroke-width="1.2"/>
100458
- `;
100459
- bandsSvg += `<path d="M ${lx + scaled(8)} ${bandY + scaled(15)} q ${scaled(5)} ${scaled(-7)} ${scaled(10)} 0" fill="none" stroke="${stroke}" stroke-width="1.2"/>
100460
- `;
100461
- } else {
100462
- const dash = dashArray(nodeStyle.stroke?.style);
100463
- bandsSvg += `<rect x="${lx}" y="${bandY + 2}" width="${scaled(26)}" height="${scaled(14)}" rx="3" fill="${nodeStyle.fill ?? palette.nodeFill}" stroke="${nodeStyle.stroke?.color ?? palette.nodeStroke}"${dash ? ` stroke-dasharray="${dash}"` : ""}/>
101144
+ const kindKeys = kindKeysSvg(paint, kinds, bandY, contentX);
101145
+ bandsSvg += kindKeys.svg;
101146
+ bandY = kindKeys.bandY;
101147
+ if (model.flows.length) {
101148
+ bandsSvg += `<line x1="${contentX}" y1="${bandY + 8}" x2="${contentX + scaled(26)}" y2="${bandY + 8}" stroke="${escAttr(defaultEdgeColor)}" stroke-width="1.3" marker-end="url(#${markerName(defaultEdgeColor)})"/>
100464
101149
  `;
100465
- }
100466
- const name = legendNames[kind];
100467
- bandsSvg += `<text x="${lx + scaled(32)}" y="${bandY + scaled(13)}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(name)}</text>
101150
+ const flowLabelText = (numbered ? legendFlowLabel + " \u2014 " + ui.numberedSuffix : legendFlowLabel) + (style.flowColor === "by-source" ? style.lang === "fr" ? " \u2014 couleur = source" : " \u2014 colour = source" : "");
101151
+ const flowKeyX = contentX + scaled(32);
101152
+ const flowKeyLines = bandLines(flowLabelText, flowKeyX, rightMargin, scaled(10));
101153
+ for (const [row, line] of flowKeyLines.entries())
101154
+ bandsSvg += `<text x="${flowKeyX}" y="${bandY + scaled(12) + row * scaled(12)}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(line)}</text>
100468
101155
  `;
100469
- lx += scaled(40) + Math.ceil(name.length * scaled(10) * RENDER_CHAR_WIDTH) + scaled(24);
100470
- if (lx > scene.width - 220) {
100471
- lx = contentX;
100472
- bandY += scaled(22);
100473
- }
101156
+ bandY += scaled(24) + (flowKeyLines.length - 1) * scaled(12);
101157
+ const lineStyleKeys = lineStyleKeysSvg(paint, bandY, contentX);
101158
+ bandsSvg += lineStyleKeys.svg;
101159
+ bandY = lineStyleKeys.bandY;
100474
101160
  }
100475
- bandY += scaled(24);
100476
- bandsSvg += `<line x1="${contentX}" y1="${bandY + 8}" x2="${contentX + scaled(26)}" y2="${bandY + 8}" stroke="${escAttr(defaultEdgeColor)}" stroke-width="1.3" marker-end="url(#${markerName(defaultEdgeColor)})"/>
100477
- `;
100478
- const flowLabelText = (numbered ? legendFlowLabel + " \u2014 " + ui.numberedSuffix : legendFlowLabel) + (style.flowColor === "by-source" ? style.lang === "fr" ? " \u2014 couleur = source" : " \u2014 colour = source" : "");
100479
- bandsSvg += `<text x="${contentX + scaled(32)}" y="${bandY + scaled(12)}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(flowLabelText)}</text>
100480
- `;
100481
- bandY += scaled(24);
100482
101161
  for (const note of model.legendNotes) {
100483
- bandsSvg += `<text x="${contentX}" y="${bandY + scaled(12)}" font-size="${scaled(10)}" fill="${palette.bandText}" font-style="italic">${esc(note)}</text>
101162
+ const lines = bandLines(note, contentX, rightMargin, scaled(10));
101163
+ for (const [row, line] of lines.entries())
101164
+ bandsSvg += `<text x="${contentX}" y="${bandY + scaled(12) + row * scaled(12)}" font-size="${scaled(10)}" fill="${palette.bandText}" font-style="italic">${esc(line)}</text>
100484
101165
  `;
100485
- bandY += scaled(20);
101166
+ bandY += scaled(20) + (lines.length - 1) * scaled(12);
100486
101167
  }
100487
101168
  };
100488
101169
  return {
@@ -100581,7 +101262,7 @@ function createEdgePainter(paint) {
100581
101262
  const flowStyle = flow?.style;
100582
101263
  const color = flowColorOf(flow);
100583
101264
  const headColor = style.flowColor === "by-source" ? color : defaultEdgeColor;
100584
- const dash = dashArray(flowStyle?.stroke?.style ?? flow?.lineStyle ?? style.flowStroke.style);
101265
+ const dash = dashArray(lineStyleOf(flow, style));
100585
101266
  const width = flowStyle?.stroke?.width ?? style.flowStroke.width;
100586
101267
  return `<path d="${edgePath(edge.pts)}" fill="none" stroke="${escAttr(color)}" stroke-width="${width}"${dash ? ` stroke-dasharray="${dash}"` : ""} marker-end="url(#${markerName(headColor)})"/>
100587
101268
  `;
@@ -100597,7 +101278,28 @@ function createEdgePainter(paint) {
100597
101278
  };
100598
101279
  return { renderEdgePath, renderEdgeLabels };
100599
101280
  }
100600
- function render(model, view, scene) {
101281
+ function svgDocument(args2) {
101282
+ const {
101283
+ width,
101284
+ height,
101285
+ fontFamily,
101286
+ background,
101287
+ arrowMarkers,
101288
+ markerSize,
101289
+ body,
101290
+ bandsSvg,
101291
+ attribution
101292
+ } = args2;
101293
+ const markers = [...arrowMarkers].map(
101294
+ ([color, markerName]) => `<marker id="${markerName}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="${markerSize}" markerHeight="${markerSize}" orient="auto-start-reverse">
101295
+ <path d="M0,0 L10,5 L0,10 z" fill="${escAttr(color)}"/></marker>`
101296
+ ).join("\n");
101297
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" font-family="${escAttr(fontFamily)},Arial,sans-serif">
101298
+ ${attribution}<defs>${markers}</defs>
101299
+ <rect width="${width}" height="${height}" fill="${escAttr(background)}"/>
101300
+ ` + body + bandsSvg + "</svg>\n";
101301
+ }
101302
+ function render(model, view, scene, options) {
100601
101303
  const style = model.style;
100602
101304
  const fonts = fontSizes(style.font.size);
100603
101305
  const { edge: edgeFontSize, node: nodeFontSize, cont: containerFontSize } = fonts;
@@ -100613,10 +101315,10 @@ function render(model, view, scene) {
100613
101315
  chipTextDy: round1(11 * fonts.scale)
100614
101316
  };
100615
101317
  const scaled = (n) => round1(n * fonts.scale);
100616
- const { palette, kinds: kindDefaults, levels: levelDefaults } = themeFor(style.theme, view);
100617
- const isDarkTheme = ["dark", "nord", "classic-dark"].includes(style.theme);
101318
+ const { palette, kinds: kindDefaults } = options?.theme ? themeFromSpec(options.theme, view) : themeFor(style.theme, view);
101319
+ const onDarkGround = options?.theme ? options.theme.dark === true : isDarkTheme(style.theme);
100618
101320
  const defaultEdgeColor = style.flowStrokeColorSet ? style.flowStroke.color : style.accent ?? palette.edge;
100619
- const sourceHue = style.flowColor === "by-source" ? assignSourceHues(model, flowPalette[isDarkTheme ? "dark" : "light"]) : /* @__PURE__ */ new Map();
101321
+ const sourceHue = style.flowColor === "by-source" ? assignSourceHues(model, flowPalette[onDarkGround ? "dark" : "light"]) : /* @__PURE__ */ new Map();
100620
101322
  const flowColorOf = (flow) => flow?.style?.stroke?.color ?? (style.flowColor === "by-source" ? sourceHue.get(flow?.from ?? "") ?? defaultEdgeColor : defaultEdgeColor);
100621
101323
  const arrowMarkers = /* @__PURE__ */ new Map();
100622
101324
  const markerName = (color) => {
@@ -100633,16 +101335,15 @@ function render(model, view, scene) {
100633
101335
  const ui = UI[style.lang] ?? UI.en;
100634
101336
  const legendNames = style.lang === "fr" ? view.legendNamesFr : view.legendNames;
100635
101337
  const legendFlowLabel = style.lang === "fr" ? view.legendFlowLabelFr : view.legendFlowLabel;
101338
+ const legendLineStyles = style.lang === "fr" ? view.legendLineStylesFr : view.legendLineStyles;
100636
101339
  const elementStyle = /* @__PURE__ */ new Map();
100637
- const elementAttr = /* @__PURE__ */ new Map();
101340
+ const elementLogo = /* @__PURE__ */ new Map();
100638
101341
  for (const entry of collectElementStyles(model.elements)) {
100639
101342
  elementStyle.set(entry.id, entry.style);
100640
- elementAttr.set(entry.id, entry.attrValue);
101343
+ if (entry.logo) elementLogo.set(entry.id, entry.logo);
100641
101344
  }
100642
101345
  const resolveStyle = (kind, id) => {
100643
- let base = kindDefaults[kind] ?? {};
100644
- const level = elementAttr.get(id);
100645
- if (kind === "trust-zone" && level && levelDefaults?.[level]) base = levelDefaults[level];
101346
+ const base = kindDefaults[kind] ?? {};
100646
101347
  const perKind = style.kind[kind] ?? {};
100647
101348
  const inline = elementStyle.get(id) ?? {};
100648
101349
  return {
@@ -100677,14 +101378,16 @@ function render(model, view, scene) {
100677
101378
  });
100678
101379
  const overlapsAfter = countLabelOverlaps();
100679
101380
  compactVertical(scene);
101381
+ fitCanvas(scene);
101382
+ const drawnLogos = /* @__PURE__ */ new Set();
100680
101383
  const { renderContainerNode, renderLeafNode } = createNodeRenderers({
100681
101384
  palette,
100682
- style,
100683
- annot,
100684
101385
  nodeFontSize,
100685
101386
  containerFontSize,
100686
101387
  resolveStyle,
100687
- elementAttr
101388
+ elementLogo,
101389
+ resolvedLogos: options?.logos,
101390
+ drawnLogos
100688
101391
  });
100689
101392
  const { renderEdgePath, renderEdgeLabels } = createEdgePainter({
100690
101393
  scene,
@@ -100715,6 +101418,7 @@ function render(model, view, scene) {
100715
101418
  ui,
100716
101419
  legendNames,
100717
101420
  legendFlowLabel,
101421
+ legendLineStyles,
100718
101422
  scaled,
100719
101423
  objectName,
100720
101424
  resolveStyle,
@@ -100727,24 +101431,28 @@ function render(model, view, scene) {
100727
101431
  if (style.legend === "auto") bands.renderLegendBand();
100728
101432
  const bandsSvg = bands.bandsSvg();
100729
101433
  const bandY = bands.bandY();
100730
- const viewWidth = scene.width, viewHeight = scene.height;
100731
- const totalHeight = bandY > viewHeight ? bandY + 14 : viewHeight;
100732
- const markerSize = style.arrows === "large" ? round1(11 * fonts.scale) : 7;
101434
+ const viewHeight = scene.height;
100733
101435
  if (arrowMarkers.size === 0) markerName(defaultEdgeColor);
100734
- const markers = [...arrowMarkers].map(
100735
- ([color, markerName2]) => `<marker id="${markerName2}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="${markerSize}" markerHeight="${markerSize}" orient="auto-start-reverse">
100736
- <path d="M0,0 L10,5 L0,10 z" fill="${escAttr(color)}"/></marker>`
100737
- ).join("\n");
100738
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${viewWidth} ${totalHeight}" font-family="${escAttr(style.font.family)},Arial,sans-serif">
100739
- <defs>${markers}</defs>
100740
- <rect width="${viewWidth}" height="${totalHeight}" fill="${escAttr(style.background ?? palette.background)}"/>
100741
- ` + body + bandsSvg + "</svg>\n";
101436
+ const svg = svgDocument({
101437
+ width: scene.width,
101438
+ height: bandY > viewHeight ? bandY + 14 : viewHeight,
101439
+ fontFamily: style.font.family,
101440
+ background: style.background ?? palette.background,
101441
+ arrowMarkers,
101442
+ markerSize: style.arrows === "large" ? round1(11 * fonts.scale) : 7,
101443
+ body,
101444
+ bandsSvg,
101445
+ // Read after the body is built, so it names the marks that were painted
101446
+ // rather than the ones the model asked for.
101447
+ attribution: logoAttributionComment(drawnLogos)
101448
+ });
100742
101449
  return { svg, overlapsBefore, overlapsAfter };
100743
101450
  }
100744
- var HOP_RADIUS, LABEL_HALO, RENDER_CHAR_WIDTH, SEC_LEVEL_FR, dashArray, round1, ADRIFT_SQ, ATTACHED_SQ;
101451
+ var HOP_RADIUS, LABEL_HALO, RENDER_CHAR_WIDTH, BAND_CONTENT_X, MIN_BAND_TEXT, bandRightMargin, dashArray, lineStyleOf, round1, ADRIFT_SQ, ATTACHED_SQ, GLYPH_BOX, GLYPHS, LOGO_BOX, legendKinds;
100745
101452
  var init_svg_render = __esm({
100746
101453
  "src/svg-render.ts"() {
100747
101454
  "use strict";
101455
+ init_views();
100748
101456
  init_themes();
100749
101457
  init_localization();
100750
101458
  init_xml_escape();
@@ -100755,19 +101463,283 @@ var init_svg_render = __esm({
100755
101463
  init_route_detour();
100756
101464
  init_readability();
100757
101465
  init_text_metrics();
101466
+ init_logo_attribution();
101467
+ init_logos();
100758
101468
  HOP_RADIUS = 5;
100759
101469
  LABEL_HALO = 4;
100760
101470
  RENDER_CHAR_WIDTH = 0.52;
100761
- SEC_LEVEL_FR = {
100762
- public: "public",
100763
- internal: "interne",
100764
- restricted: "restreint",
100765
- secret: "secret"
100766
- };
101471
+ BAND_CONTENT_X = 150;
101472
+ MIN_BAND_TEXT = 200;
101473
+ bandRightMargin = (scene) => scene.width - 20;
100767
101474
  dashArray = (lineStyle) => lineStyle === "dashed" ? "5 3" : lineStyle === "dotted" ? "2 2.5" : void 0;
101475
+ lineStyleOf = (flow, style) => flow?.style?.stroke?.style ?? flow?.lineStyle ?? style.flowStroke.style;
100768
101476
  round1 = (n) => Math.round(n * 10) / 10;
100769
101477
  ADRIFT_SQ = 20 * 20;
100770
101478
  ATTACHED_SQ = 6 * 6;
101479
+ GLYPH_BOX = { width: 18, height: 16, left: 6, top: 7 };
101480
+ GLYPHS = {
101481
+ // Padlock: authentication is a check something must pass.
101482
+ auth: ({ x, y, r, line, stroke }) => `<rect x="${x(3)}" y="${y(7)}" width="${r(12)}" height="${r(9)}" rx="${r(2)}" ${line}/><path d="M ${x(6)} ${y(7)} v ${-r(3)} a ${r(3)} ${r(3)} 0 0 1 ${r(6)} 0 v ${r(3)}" ${line}/><circle cx="${x(9)}" cy="${y(11)}" r="${r(1.5)}" fill="${stroke}"/>`,
101483
+ // Two posts with traffic passing between them: a gateway routes, it does not block.
101484
+ gateway: ({ x, y, r, line }) => `<path d="M ${x(2)} ${y(1)} V ${y(15)} M ${x(16)} ${y(1)} V ${y(15)}" ${line}/><path d="M ${x(4)} ${y(8)} H ${x(14)}" ${line}/><path d="M ${x(11)} ${y(5)} l ${r(3)} ${r(3)} l ${-r(3)} ${r(3)}" ${line}/>`,
101485
+ // ID badge: an identity provider issues who-you-are, it does not check it.
101486
+ idp: ({ x, y, r, line }) => `<rect x="${x(3)}" y="${y(2)}" width="${r(12)}" height="${r(13)}" rx="${r(2)}" ${line}/><path d="M ${x(7)} ${y(2)} H ${x(11)}" ${line}/><circle cx="${x(9)}" cy="${y(7)}" r="${r(2)}" ${line}/><path d="M ${x(5)} ${y(13)} q ${r(4)} ${-r(4)} ${r(8)} 0" ${line}/>`,
101487
+ // Brick wall: a firewall is a barrier, and no other kind reads as one.
101488
+ firewall: ({ x, y, r, line }) => `<rect x="${x(2)}" y="${y(2)}" width="${r(14)}" height="${r(12)}" rx="${r(1)}" ${line}/><path d="M ${x(2)} ${y(6)} H ${x(16)} M ${x(2)} ${y(10)} H ${x(16)}" ${line}/><path d="M ${x(9)} ${y(2)} V ${y(6)} M ${x(6)} ${y(6)} V ${y(10)} M ${x(12)} ${y(6)} V ${y(10)} M ${x(9)} ${y(10)} V ${y(14)}" ${line}/>`,
101489
+ // Monitor on a stand: a device is the machine a person works at. The screen is
101490
+ // left empty on purpose — that is what separates it from the firewall's wall
101491
+ // at legend size, where both are a rectangle and little else survives.
101492
+ device: ({ x, y, r, line }) => `<rect x="${x(2)}" y="${y(1)}" width="${r(14)}" height="${r(9)}" rx="${r(1)}" ${line}/><path d="M ${x(9)} ${y(10)} V ${y(13)}" ${line}/><path d="M ${x(4)} ${y(13)} H ${x(14)}" ${line}/>`
101493
+ };
101494
+ LOGO_BOX = { size: 18, right: 7, top: 6 };
101495
+ legendKinds = (paint) => [...new Set(paint.scene.nodes.map((node) => node.kind))].filter(
101496
+ (kind) => paint.legendNames[kind] && (kind !== "actor" || paint.view.actorLegend)
101497
+ );
101498
+ }
101499
+ });
101500
+
101501
+ // src/css-colours.ts
101502
+ var HEX, NUMBER, PERCENT, AMOUNT, ANGLE, commaForm, spaceForm, RGB, HSL, NUMERIC_COLOUR, COLOUR_KEYWORDS, isColour;
101503
+ var init_css_colours = __esm({
101504
+ "src/css-colours.ts"() {
101505
+ "use strict";
101506
+ HEX = "#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})";
101507
+ NUMBER = String.raw`[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?`;
101508
+ PERCENT = `${NUMBER}%`;
101509
+ AMOUNT = `${NUMBER}%?`;
101510
+ ANGLE = `${NUMBER}(?:deg|grad|rad|turn)?`;
101511
+ commaForm = (first, second, third) => `${first}\\s*,\\s*${second}\\s*,\\s*${third}(?:\\s*,\\s*${AMOUNT})?`;
101512
+ spaceForm = (first, second, third) => `${first}\\s+${second}\\s+${third}(?:\\s*/\\s*${AMOUNT})?`;
101513
+ RGB = `rgba?\\(\\s*(?:${commaForm(NUMBER, NUMBER, NUMBER)}|${commaForm(PERCENT, PERCENT, PERCENT)}|${spaceForm(AMOUNT, AMOUNT, AMOUNT)})\\s*\\)`;
101514
+ HSL = `hsla?\\(\\s*(?:${commaForm(ANGLE, PERCENT, PERCENT)}|${spaceForm(ANGLE, AMOUNT, AMOUNT)})\\s*\\)`;
101515
+ NUMERIC_COLOUR = new RegExp(`^(?:${HEX}|${RGB}|${HSL})$`, "i");
101516
+ COLOUR_KEYWORDS = /* @__PURE__ */ new Set([
101517
+ "currentcolor",
101518
+ "transparent",
101519
+ "aliceblue",
101520
+ "antiquewhite",
101521
+ "aqua",
101522
+ "aquamarine",
101523
+ "azure",
101524
+ "beige",
101525
+ "bisque",
101526
+ "black",
101527
+ "blanchedalmond",
101528
+ "blue",
101529
+ "blueviolet",
101530
+ "brown",
101531
+ "burlywood",
101532
+ "cadetblue",
101533
+ "chartreuse",
101534
+ "chocolate",
101535
+ "coral",
101536
+ "cornflowerblue",
101537
+ "cornsilk",
101538
+ "crimson",
101539
+ "cyan",
101540
+ "darkblue",
101541
+ "darkcyan",
101542
+ "darkgoldenrod",
101543
+ "darkgray",
101544
+ "darkgreen",
101545
+ "darkgrey",
101546
+ "darkkhaki",
101547
+ "darkmagenta",
101548
+ "darkolivegreen",
101549
+ "darkorange",
101550
+ "darkorchid",
101551
+ "darkred",
101552
+ "darksalmon",
101553
+ "darkseagreen",
101554
+ "darkslateblue",
101555
+ "darkslategray",
101556
+ "darkslategrey",
101557
+ "darkturquoise",
101558
+ "darkviolet",
101559
+ "deeppink",
101560
+ "deepskyblue",
101561
+ "dimgray",
101562
+ "dimgrey",
101563
+ "dodgerblue",
101564
+ "firebrick",
101565
+ "floralwhite",
101566
+ "forestgreen",
101567
+ "fuchsia",
101568
+ "gainsboro",
101569
+ "ghostwhite",
101570
+ "gold",
101571
+ "goldenrod",
101572
+ "gray",
101573
+ "green",
101574
+ "greenyellow",
101575
+ "grey",
101576
+ "honeydew",
101577
+ "hotpink",
101578
+ "indianred",
101579
+ "indigo",
101580
+ "ivory",
101581
+ "khaki",
101582
+ "lavender",
101583
+ "lavenderblush",
101584
+ "lawngreen",
101585
+ "lemonchiffon",
101586
+ "lightblue",
101587
+ "lightcoral",
101588
+ "lightcyan",
101589
+ "lightgoldenrodyellow",
101590
+ "lightgray",
101591
+ "lightgreen",
101592
+ "lightgrey",
101593
+ "lightpink",
101594
+ "lightsalmon",
101595
+ "lightseagreen",
101596
+ "lightskyblue",
101597
+ "lightslategray",
101598
+ "lightslategrey",
101599
+ "lightsteelblue",
101600
+ "lightyellow",
101601
+ "lime",
101602
+ "limegreen",
101603
+ "linen",
101604
+ "magenta",
101605
+ "maroon",
101606
+ "mediumaquamarine",
101607
+ "mediumblue",
101608
+ "mediumorchid",
101609
+ "mediumpurple",
101610
+ "mediumseagreen",
101611
+ "mediumslateblue",
101612
+ "mediumspringgreen",
101613
+ "mediumturquoise",
101614
+ "mediumvioletred",
101615
+ "midnightblue",
101616
+ "mintcream",
101617
+ "mistyrose",
101618
+ "moccasin",
101619
+ "navajowhite",
101620
+ "navy",
101621
+ "oldlace",
101622
+ "olive",
101623
+ "olivedrab",
101624
+ "orange",
101625
+ "orangered",
101626
+ "orchid",
101627
+ "palegoldenrod",
101628
+ "palegreen",
101629
+ "paleturquoise",
101630
+ "palevioletred",
101631
+ "papayawhip",
101632
+ "peachpuff",
101633
+ "peru",
101634
+ "pink",
101635
+ "plum",
101636
+ "powderblue",
101637
+ "purple",
101638
+ "rebeccapurple",
101639
+ "red",
101640
+ "rosybrown",
101641
+ "royalblue",
101642
+ "saddlebrown",
101643
+ "salmon",
101644
+ "sandybrown",
101645
+ "seagreen",
101646
+ "seashell",
101647
+ "sienna",
101648
+ "silver",
101649
+ "skyblue",
101650
+ "slateblue",
101651
+ "slategray",
101652
+ "slategrey",
101653
+ "snow",
101654
+ "springgreen",
101655
+ "steelblue",
101656
+ "tan",
101657
+ "teal",
101658
+ "thistle",
101659
+ "tomato",
101660
+ "turquoise",
101661
+ "violet",
101662
+ "wheat",
101663
+ "white",
101664
+ "whitesmoke",
101665
+ "yellow",
101666
+ "yellowgreen"
101667
+ ]);
101668
+ isColour = (value) => NUMERIC_COLOUR.test(value) || COLOUR_KEYWORDS.has(value.toLowerCase());
101669
+ }
101670
+ });
101671
+
101672
+ // src/theme-spec.ts
101673
+ function checkEntry(path, value, arity) {
101674
+ if (arity === 1) {
101675
+ if (typeof value !== "string") {
101676
+ throw new ThemeSpecError(`\`${path}\` must be a colour, got ${describe(value)}`);
101677
+ }
101678
+ } else if (!Array.isArray(value) || value.length !== arity) {
101679
+ throw new ThemeSpecError(`\`${path}\` must be ${arity} colours, got ${describe(value)}`);
101680
+ }
101681
+ for (const colour of Array.isArray(value) ? value : [value]) {
101682
+ if (typeof colour !== "string" || !isColour(colour)) {
101683
+ throw new ThemeSpecError(`\`${path}\`: \`${String(colour)}\` is not a colour`);
101684
+ }
101685
+ }
101686
+ }
101687
+ function resolveThemeSpec(input) {
101688
+ if (!isObject(input)) throw new ThemeSpecError("a theme must be an object");
101689
+ const base = input.extends ?? DEFAULT_BASE;
101690
+ if (typeof base !== "string") throw new ThemeSpecError("`extends` must be a theme name");
101691
+ const baseSpec = Object.hasOwn(THEME_SPECS, base) ? THEME_SPECS[base] : void 0;
101692
+ if (!baseSpec) {
101693
+ throw new ThemeSpecError(
101694
+ `extends unknown theme \`${base}\` \u2014 pick one of: ${Object.keys(THEME_SPECS).join(", ")}`
101695
+ );
101696
+ }
101697
+ if ("dark" in input && typeof input.dark !== "boolean") {
101698
+ throw new ThemeSpecError("`dark` must be true or false");
101699
+ }
101700
+ for (const [section, value] of Object.entries(input)) {
101701
+ if (section === "extends" || section === "dark") continue;
101702
+ if (!SECTIONS.includes(section)) {
101703
+ throw new ThemeSpecError(
101704
+ `unknown section \`${section}\` \u2014 a theme overrides ${SECTIONS.join(", ")}`
101705
+ );
101706
+ }
101707
+ if (!isObject(value)) {
101708
+ throw new ThemeSpecError(
101709
+ `\`${section}\` must be an object of colours, got ${describe(value)}`
101710
+ );
101711
+ }
101712
+ for (const [key, colour] of Object.entries(value)) {
101713
+ checkEntry(`${section}.${key}`, colour, arityOf(section, key));
101714
+ }
101715
+ }
101716
+ return merge(baseSpec, input);
101717
+ }
101718
+ var DEFAULT_BASE, ThemeSpecError, isObject, merge, SECTIONS, arityOf, describe;
101719
+ var init_theme_spec = __esm({
101720
+ "src/theme-spec.ts"() {
101721
+ "use strict";
101722
+ init_css_colours();
101723
+ init_themes();
101724
+ DEFAULT_BASE = "light";
101725
+ ThemeSpecError = class extends Error {
101726
+ };
101727
+ isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
101728
+ merge = (base, patch) => {
101729
+ const merged = { ...base };
101730
+ for (const [key, value] of Object.entries(patch)) {
101731
+ if (key === "extends") continue;
101732
+ const current = merged[key];
101733
+ merged[key] = isObject(value) && isObject(current) ? { ...current, ...value } : value;
101734
+ }
101735
+ return merged;
101736
+ };
101737
+ SECTIONS = ["pal", "accentColors"];
101738
+ arityOf = (section, key) => {
101739
+ if (section === "pal") return key === "chip" ? 3 : key === "badge" ? 2 : 1;
101740
+ return 1;
101741
+ };
101742
+ describe = (value) => Array.isArray(value) ? `an array of ${value.length}` : typeof value;
100771
101743
  }
100772
101744
  });
100773
101745
 
@@ -100834,8 +101806,9 @@ function matrixMd(matrix) {
100834
101806
  for (const row of matrix.rows) outLines.push("| " + row.cells.map(mdCell).join(" | ") + " |");
100835
101807
  return outLines.join("\n") + "\n";
100836
101808
  }
100837
- function matrixSvg(matrix) {
100838
- const pal = palettes[matrix.style.theme] ?? lightPalette;
101809
+ function matrixSvg(matrix, options) {
101810
+ const requested = options?.theme;
101811
+ const pal = typeof requested === "object" ? paletteFromSpec(resolveThemeSpec(requested)) : paletteFor(requested ?? matrix.style.theme);
100839
101812
  const title = matrix.heading;
100840
101813
  const headers = matrix.columns.map((column) => column.label);
100841
101814
  const cells = matrix.rows.map((row) => row.cells);
@@ -100903,6 +101876,7 @@ var init_flow_matrix = __esm({
100903
101876
  "use strict";
100904
101877
  init_localization();
100905
101878
  init_themes();
101879
+ init_theme_spec();
100906
101880
  init_xml_escape();
100907
101881
  init_text_metrics();
100908
101882
  COLUMN_PRESENTATION = {
@@ -100939,9 +101913,81 @@ var init_flow_matrix = __esm({
100939
101913
  }
100940
101914
  });
100941
101915
 
101916
+ // src/logo-files.ts
101917
+ import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
101918
+ import { dirname, resolve, extname } from "node:path";
101919
+ function resolveLogoFiles(model, sourceFile) {
101920
+ const logos = /* @__PURE__ */ new Map();
101921
+ const diagnostics = [];
101922
+ const base = dirname(resolve(sourceFile));
101923
+ for (const element of model.elements.flatMap((root) => subtreeElements(root))) {
101924
+ const logo = element.logo;
101925
+ if (logo?.source !== "file") continue;
101926
+ const warn = (message, help) => diagnostics.push({
101927
+ code: "W0580",
101928
+ severity: "warning",
101929
+ message,
101930
+ span: logo.span,
101931
+ help
101932
+ });
101933
+ const mime = MIME_BY_EXT[extname(logo.value).toLowerCase()];
101934
+ if (!mime) {
101935
+ warn(
101936
+ `unsupported logo file type \`${extname(logo.value) || logo.value}\``,
101937
+ `use one of: ${Object.keys(MIME_BY_EXT).join(", ")}`
101938
+ );
101939
+ continue;
101940
+ }
101941
+ const path = resolve(base, logo.value);
101942
+ let bytes;
101943
+ try {
101944
+ const fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
101945
+ try {
101946
+ const stat = fstatSync(fd);
101947
+ if (!stat.isFile()) {
101948
+ warn(`logo file \`${logo.value}\` is not a regular file`, "point at an image file");
101949
+ continue;
101950
+ }
101951
+ if (stat.size > MAX_BYTES) {
101952
+ warn(
101953
+ `logo file is ${Math.round(stat.size / 1024)} KB, over the ${MAX_BYTES / 1024} KB limit`,
101954
+ "a corner mark needs very little \u2014 export it smaller, or use a built-in"
101955
+ );
101956
+ continue;
101957
+ }
101958
+ bytes = Buffer.alloc(stat.size);
101959
+ const read = readSync(fd, bytes, 0, stat.size, 0);
101960
+ if (read < stat.size) bytes = bytes.subarray(0, read);
101961
+ } finally {
101962
+ closeSync(fd);
101963
+ }
101964
+ } catch {
101965
+ warn(`cannot read logo file \`${logo.value}\``, `looked in \`${base}\``);
101966
+ continue;
101967
+ }
101968
+ logos.set(element.id, `data:${mime};base64,${bytes.toString("base64")}`);
101969
+ }
101970
+ return { logos, diagnostics };
101971
+ }
101972
+ var MIME_BY_EXT, MAX_BYTES;
101973
+ var init_logo_files = __esm({
101974
+ "src/logo-files.ts"() {
101975
+ "use strict";
101976
+ init_element_tree();
101977
+ MIME_BY_EXT = {
101978
+ ".svg": "image/svg+xml",
101979
+ ".png": "image/png",
101980
+ ".jpg": "image/jpeg",
101981
+ ".jpeg": "image/jpeg",
101982
+ ".webp": "image/webp"
101983
+ };
101984
+ MAX_BYTES = 256 * 1024;
101985
+ }
101986
+ });
101987
+
100942
101988
  // src/watch.ts
100943
101989
  import { watch as fsWatch, readFileSync, writeFileSync, existsSync } from "node:fs";
100944
- import { dirname, basename } from "node:path";
101990
+ import { dirname as dirname2, basename } from "node:path";
100945
101991
  function errorPanelSvg(file, diagnostics) {
100946
101992
  const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").slice(0, 10);
100947
101993
  const more = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length - errors.length;
@@ -100966,7 +102012,7 @@ function errorPanelSvg(file, diagnostics) {
100966
102012
  `;
100967
102013
  return out + "</svg>\n";
100968
102014
  }
100969
- function watchCommand(file, outFile) {
102015
+ function watchCommand(file, outFile, theme) {
100970
102016
  if (!existsSync(file)) {
100971
102017
  console.error(`error: file not found \`${file}\``);
100972
102018
  process.exit(2);
@@ -100982,6 +102028,7 @@ function watchCommand(file, outFile) {
100982
102028
  const src = readFileSync(file, "utf8");
100983
102029
  const { model, diags } = parse(src);
100984
102030
  diags.push(...validate(model));
102031
+ if (theme) model.style.theme = theme;
100985
102032
  const errors = diags.filter((diagnostic) => diagnostic.severity === "error");
100986
102033
  const stamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
100987
102034
  console.clear();
@@ -101000,7 +102047,9 @@ function watchCommand(file, outFile) {
101000
102047
  const view = views[model.type];
101001
102048
  const scene = await layout(model, view);
101002
102049
  diags.push(...attachSideDiagnostics(scene, model));
101003
- const { svg, overlapsAfter } = render(model, view, scene);
102050
+ const { logos, diagnostics: logoDiagnostics } = resolveLogoFiles(model, file);
102051
+ diags.push(...logoDiagnostics);
102052
+ const { svg, overlapsAfter } = render(model, view, scene, { logos });
101004
102053
  writeFileSync(outFile, svg);
101005
102054
  if (diags.length)
101006
102055
  console.log(renderHuman(file, src, diags, process.stdout.isTTY ?? false) + "\n");
@@ -101033,7 +102082,7 @@ function watchCommand(file, outFile) {
101033
102082
  }
101034
102083
  };
101035
102084
  let timer = null;
101036
- fsWatch(dirname(file) || ".", (_event, fileName) => {
102085
+ fsWatch(dirname2(file) || ".", (_event, fileName) => {
101037
102086
  if (fileName && fileName !== basename(file)) return;
101038
102087
  if (timer) clearTimeout(timer);
101039
102088
  timer = setTimeout(rebuild, 120);
@@ -101050,28 +102099,113 @@ var init_watch = __esm({
101050
102099
  init_svg_render();
101051
102100
  init_views();
101052
102101
  init_xml_escape();
102102
+ init_logo_files();
102103
+ }
102104
+ });
102105
+
102106
+ // src/theme-file.ts
102107
+ import { readFileSync as readFileSync2 } from "node:fs";
102108
+ function loadThemeFile(path) {
102109
+ let raw;
102110
+ try {
102111
+ raw = readFileSync2(path, "utf8");
102112
+ } catch {
102113
+ throw new ThemeFileError(`cannot read theme file \`${path}\``);
102114
+ }
102115
+ let parsed;
102116
+ try {
102117
+ parsed = JSON.parse(raw);
102118
+ } catch (error) {
102119
+ throw new ThemeFileError(
102120
+ `\`${path}\` is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
102121
+ );
102122
+ }
102123
+ let spec;
102124
+ try {
102125
+ spec = resolveThemeSpec(parsed);
102126
+ } catch (error) {
102127
+ if (error instanceof ThemeSpecError) throw new ThemeFileError(`\`${path}\`: ${error.message}`);
102128
+ throw error;
102129
+ }
102130
+ const name = (path.split(/[\\/]/).pop() ?? path).replace(/\.json$/i, "");
102131
+ if (!name) throw new ThemeFileError(`\`${path}\` has no usable theme name`);
102132
+ return { name, spec };
102133
+ }
102134
+ var ThemeFileError;
102135
+ var init_theme_file = __esm({
102136
+ "src/theme-file.ts"() {
102137
+ "use strict";
102138
+ init_theme_spec();
102139
+ ThemeFileError = class extends Error {
102140
+ };
102141
+ }
102142
+ });
102143
+
102144
+ // src/notice.ts
102145
+ function notice({ bun = embedsBun() } = {}) {
102146
+ return COMMON + (bun ? BUN_NOTICE : "") + "\n" + FOOTER;
102147
+ }
102148
+ var ELKJS_VERSION, SIMPLE_ICONS_VERSION, BUN_VERSION, NOTICES_URL, COMMON, BUN_NOTICE, FOOTER, embedsBun;
102149
+ var init_notice = __esm({
102150
+ "src/notice.ts"() {
102151
+ "use strict";
102152
+ ELKJS_VERSION = "0.12.0";
102153
+ SIMPLE_ICONS_VERSION = "16.29.0";
102154
+ BUN_VERSION = "1.4.0";
102155
+ NOTICES_URL = "https://github.com/R0kshan/cairn/blob/main/THIRD-PARTY-NOTICES.md";
102156
+ COMMON = `cairn \u2014 Apache-2.0 \u2014 https://github.com/R0kshan/cairn
102157
+ Copyright 2026 R\xF8kshan.
102158
+
102159
+ This artifact contains third-party code and artwork:
102160
+
102161
+ elkjs ${ELKJS_VERSION} \u2014 (c) 2017 Kiel University and others.
102162
+ Dual-licensed EPL-2.0 OR GPL-3.0-or-later; cairn elects EPL-2.0.
102163
+ Full text: licenses/elkjs-EPL-2.0.md \u2014 https://www.eclipse.org/legal/epl-2.0
102164
+
102165
+ Simple Icons ${SIMPLE_ICONS_VERSION} artwork \u2014 CC0-1.0 project-wide.
102166
+ Six of the vendored icons carry their own terms instead \u2014 MIT,
102167
+ BSD-3-Clause, CC-BY-4.0 or Apache-2.0 \u2014 and are attributed individually.
102168
+ Full texts: licenses/`;
102169
+ BUN_NOTICE = `
102170
+
102171
+ Bun ${BUN_VERSION} runtime \u2014 MIT \u2014 embedded by \`bun build --compile\`.
102172
+ It statically links JavaScriptCore/WebKit, which is LGPL-2.1 in part,
102173
+ alongside BSD- and Apache-licensed components. Bun's own LICENSE.md
102174
+ enumerates all of them verbatim: licenses/bun-LICENSE.md
102175
+ Relinking under LGPL-2.1 \xA76: see THIRD-PARTY-NOTICES.md.`;
102176
+ FOOTER = `
102177
+ Per-icon attribution and the full licence texts ship with this artifact:
102178
+ the package root (npm), the app directory (Scoop), or
102179
+ share/doc/cairn (Homebrew, install.sh). Also at
102180
+ ${NOTICES_URL}`;
102181
+ embedsBun = () => typeof CAIRN_EMBEDS_BUN !== "undefined" && CAIRN_EMBEDS_BUN;
101053
102182
  }
101054
102183
  });
101055
102184
 
101056
102185
  // src/cli.ts
101057
102186
  var cli_exports = {};
101058
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
102187
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
101059
102188
  function usage() {
101060
102189
  console.log(`cairn \u2014 architecture diagrams as code
101061
102190
 
101062
102191
  Usage:
101063
- cairn new (--logical-architecture|-L|-A|-I|-S) <file.cairn> scaffold a typed template
102192
+ cairn new (--logical-architecture|-L|-A|-I) <file.cairn> scaffold a typed template
101064
102193
  (-L logical, -A application,
101065
- -I infrastructure, -S security)
102194
+ -I infrastructure)
101066
102195
  cairn validate <file.cairn> [--format json] [--strict]
101067
- cairn build <file.cairn> [-o output.svg]
101068
- cairn matrix <file.cairn> [--format csv|md|svg] [-o out] matrice des flux techniques
102196
+ cairn build <file.cairn> [-o output.svg] [--theme <name|file.json>]
102197
+ cairn matrix <file.cairn> [--format csv|md|svg] [-o out] [--theme <name|file.json>]
102198
+ matrice des flux techniques
101069
102199
  (default csv; honors style { lang })
101070
- cairn watch <file.cairn> [-o output.svg] rebuild on save; SVG stays fresh
102200
+ cairn watch <file.cairn> [-o output.svg] [--theme <name|file.json>]
102201
+ rebuild on save; SVG stays fresh
101071
102202
  (error panel on failure) for an
101072
102203
  editor auto-refresh preview
101073
102204
  cairn explain <code> rule rationale (e.g. E0203)
102205
+ cairn logos list the built-in \`logo:\` names
102206
+ cairn themes list the built-in theme names
101074
102207
  cairn version | --version | -v print the installed version
102208
+ cairn version --licenses third-party notices this build carries
101075
102209
  `);
101076
102210
  process.exit(2);
101077
102211
  }
@@ -101080,7 +102214,7 @@ function loadAndCheck(file) {
101080
102214
  console.error(`error: file not found \`${file}\``);
101081
102215
  process.exit(2);
101082
102216
  }
101083
- const src = readFileSync2(file, "utf8");
102217
+ const src = readFileSync3(file, "utf8");
101084
102218
  const { model, diags } = parse(src);
101085
102219
  diags.push(...validate(model));
101086
102220
  return { src, model, diagnostics: diags };
@@ -101111,13 +102245,41 @@ function densityReport(disposition, scene, typeSpan) {
101111
102245
  }
101112
102246
  };
101113
102247
  }
102248
+ function themeFromArgs() {
102249
+ const index = args.indexOf("--theme");
102250
+ if (index === -1) return void 0;
102251
+ const value = args[index + 1];
102252
+ if (!value || value.startsWith("-")) {
102253
+ console.error("error: `--theme` needs a theme name or a path to a .json theme file");
102254
+ process.exit(2);
102255
+ }
102256
+ if (!value.toLowerCase().endsWith(".json")) {
102257
+ if (!themeNames.includes(value)) {
102258
+ console.error(
102259
+ `error: unknown theme \`${value}\`
102260
+ available: ${themeNames.join(", ")}
102261
+ or pass a path to a .json theme file`
102262
+ );
102263
+ process.exit(2);
102264
+ }
102265
+ return value;
102266
+ }
102267
+ try {
102268
+ const { name, spec } = loadThemeFile(value);
102269
+ registerTheme(name, spec);
102270
+ return name;
102271
+ } catch (error) {
102272
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
102273
+ process.exit(2);
102274
+ }
102275
+ }
101114
102276
  function exitIfErrors(file, src, diagnostics) {
101115
102277
  if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
101116
102278
  console.error(renderHuman(file, src, diagnostics, process.stderr.isTTY ?? false));
101117
102279
  process.exit(1);
101118
102280
  }
101119
102281
  }
101120
- var TYPE_FLAGS, TEMPLATE_LOGICAL, TEMPLATE_APPLICATION, TEMPLATE_INFRASTRUCTURE, TEMPLATE_SECURITY, TEMPLATES, args, command, version, VALUE_FLAGS, positionalFile, PRINT_FRAMES;
102282
+ var TYPE_FLAGS, TEMPLATE_LOGICAL, TEMPLATE_APPLICATION, TEMPLATE_INFRASTRUCTURE, TEMPLATES, args, command, version, VALUE_FLAGS, positionalFile, PRINT_FRAMES;
101121
102283
  var init_cli = __esm({
101122
102284
  "src/cli.ts"() {
101123
102285
  "use strict";
@@ -101130,15 +102292,18 @@ var init_cli = __esm({
101130
102292
  init_views();
101131
102293
  init_ast();
101132
102294
  init_watch();
102295
+ init_logo_files();
102296
+ init_theme_file();
102297
+ init_themes();
102298
+ init_logos();
102299
+ init_notice();
101133
102300
  TYPE_FLAGS = {
101134
102301
  "--logical-architecture": "logical",
101135
102302
  "-L": "logical",
101136
102303
  "--application-architecture": "application",
101137
102304
  "-A": "application",
101138
102305
  "--infrastructure-architecture": "infrastructure",
101139
- "-I": "infrastructure",
101140
- "--security-architecture": "security",
101141
- "-S": "security"
102306
+ "-I": "infrastructure"
101142
102307
  };
101143
102308
  TEMPLATE_LOGICAL = `diagram logical "Diagram title"
101144
102309
 
@@ -101259,45 +102424,16 @@ CORE_I -> PARTNER : "Nightly export" (SFTP/22)
101259
102424
  # Export the flow matrix beside the physical diagram (French DA deliverable):
101260
102425
  # cairn matrix this-file.cairn --format csv # or md | svg
101261
102426
  # Add \`style { lang: fr }\` above to get French column headers (N\xB0, Protocole\u2026).
101262
- `;
101263
- TEMPLATE_SECURITY = `diagram security "Diagram title"
101264
-
101265
- # A security diagram shows trust zones (each with a sensitivity level), the
101266
- # filtering/security nodes between them, and the sensitive assets they protect.
101267
- # Zones band left\u2192right in declaration order (most exposed \u2192 most protected).
101268
- # Levels, least\u2192most trusted: public | internal | restricted | secret.
101269
-
101270
- trust-zone INTERNET "Internet" (public) {
101271
- security-node WAF "WAF / reverse proxy"
101272
- }
101273
-
101274
- trust-zone LAN "Internal zone" (internal) {
101275
- asset APP "Business application"
101276
- }
101277
-
101278
- trust-zone DATA "Data zone" (restricted) {
101279
- asset DB "Customer database"
101280
- }
101281
-
101282
- external USERS "End users"
101283
-
101284
- # ---- security flows (cross-zone flows should be filtered + encrypted) ----
101285
- USERS -> WAF : "HTTPS access" (TLS1.3)
101286
- WAF -> APP : "Filtered traffic" (mTLS)
101287
- APP -> DB : "Read/write" (TLS1.3)
101288
-
101289
- # W0560 warns on any flow entering a more-trusted zone without a security-node.
101290
102427
  `;
101291
102428
  TEMPLATES = {
101292
102429
  logical: TEMPLATE_LOGICAL,
101293
102430
  application: TEMPLATE_APPLICATION,
101294
- infrastructure: TEMPLATE_INFRASTRUCTURE,
101295
- security: TEMPLATE_SECURITY
102431
+ infrastructure: TEMPLATE_INFRASTRUCTURE
101296
102432
  };
101297
102433
  args = process.argv.slice(2);
101298
102434
  command = args[0];
101299
- version = true ? "1.0.0-RC13" : pkg.version;
101300
- VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "--format"]);
102435
+ version = true ? "1.0.0-RC15" : pkg.version;
102436
+ VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "--format", "--theme"]);
101301
102437
  positionalFile = () => {
101302
102438
  for (let index = 0; index < args.length; index++) {
101303
102439
  const arg = args[index];
@@ -101315,7 +102451,24 @@ APP -> DB : "Read/write" (TLS1.3)
101315
102451
  page: { width: 700, height: 1e3, name: "A4 page" }
101316
102452
  };
101317
102453
  if (command === "version" || command === "--version" || command === "-v") {
101318
- console.log(`cairn v${version}`);
102454
+ const LICENCE_FLAGS = /* @__PURE__ */ new Set(["--licenses", "--licences"]);
102455
+ const unknown = args.slice(1).filter((arg) => arg.startsWith("-") && !LICENCE_FLAGS.has(arg));
102456
+ if (unknown.length) {
102457
+ console.error(
102458
+ `unknown flag${unknown.length > 1 ? "s" : ""} for \`cairn version\`: ${unknown.join(", ")}
102459
+ the only one it takes is \`--licenses\` (third-party notices for this build)`
102460
+ );
102461
+ process.exit(2);
102462
+ }
102463
+ if (args.some((arg) => LICENCE_FLAGS.has(arg))) {
102464
+ console.log(`cairn v${version}
102465
+ `);
102466
+ console.log(notice());
102467
+ } else {
102468
+ console.log(`cairn v${version}`);
102469
+ if (process.stdout.isTTY)
102470
+ console.log("Apache-2.0, and bundles third-party code \u2014 `cairn version --licenses`");
102471
+ }
101319
102472
  } else if (command === "validate") {
101320
102473
  const file = positionalFile();
101321
102474
  if (!file) usage();
@@ -101332,12 +102485,16 @@ APP -> DB : "Read/write" (TLS1.3)
101332
102485
  } else if (command === "build") {
101333
102486
  const file = positionalFile();
101334
102487
  if (!file) usage();
102488
+ const themeOverride = themeFromArgs();
101335
102489
  const outFile = resolveOutputPath(file, ".svg");
101336
102490
  const { src, model, diagnostics } = loadAndCheck(file);
101337
102491
  exitIfErrors(file, src, diagnostics);
102492
+ if (themeOverride) model.style.theme = themeOverride;
101338
102493
  const view = views[model.type];
101339
102494
  layout(model, view).then((scene) => {
101340
- const { svg, overlapsBefore, overlapsAfter } = render(model, view, scene);
102495
+ const { logos, diagnostics: logoDiagnostics } = resolveLogoFiles(model, file);
102496
+ diagnostics.push(...logoDiagnostics);
102497
+ const { svg, overlapsBefore, overlapsAfter } = render(model, view, scene, { logos });
101341
102498
  writeFileSync2(outFile, svg);
101342
102499
  const { fitInfo, diagnostic } = densityReport(
101343
102500
  model.style.disposition,
@@ -101360,6 +102517,7 @@ APP -> DB : "Read/write" (TLS1.3)
101360
102517
  } else if (command === "matrix") {
101361
102518
  const file = positionalFile();
101362
102519
  if (!file) usage();
102520
+ const themeOverride = themeFromArgs();
101363
102521
  const formatIndex = args.indexOf("--format");
101364
102522
  const format = formatIndex >= 0 ? args[formatIndex + 1] : "csv";
101365
102523
  if (!["csv", "md", "svg"].includes(format)) {
@@ -101372,6 +102530,7 @@ APP -> DB : "Read/write" (TLS1.3)
101372
102530
  console.error(`error: \`${file}\` declares no flows \u2014 nothing to tabulate`);
101373
102531
  process.exit(1);
101374
102532
  }
102533
+ if (themeOverride) model.style.theme = themeOverride;
101375
102534
  const ext = format === "svg" ? "svg" : format === "md" ? "md" : "csv";
101376
102535
  const outFile = resolveOutputPath(file, ".flow." + ext);
101377
102536
  const lang = model.style.lang;
@@ -101384,7 +102543,7 @@ APP -> DB : "Read/write" (TLS1.3)
101384
102543
  } else if (command === "watch") {
101385
102544
  const file = positionalFile();
101386
102545
  if (!file) usage();
101387
- watchCommand(file, resolveOutputPath(file, ".svg"));
102546
+ watchCommand(file, resolveOutputPath(file, ".svg"), themeFromArgs());
101388
102547
  } else if (command === "new") {
101389
102548
  const type = args.map((arg) => TYPE_FLAGS[arg]).find(Boolean);
101390
102549
  const file = positionalFile();
@@ -101408,6 +102567,25 @@ APP -> DB : "Read/write" (TLS1.3)
101408
102567
  console.log(
101409
102568
  `\u2713 ${file} created (${type} view) \u2014 fill in the sections, then run \`cairn validate ${file}\``
101410
102569
  );
102570
+ } else if (command === "logos") {
102571
+ const width = LOGO_NAMES.reduce((widest, name) => Math.max(widest, name.length), 0) + 2;
102572
+ const perLine = Math.max(1, Math.floor(78 / width));
102573
+ console.log(`${LOGO_NAMES.length} built-in logos \u2014 use as \`logo: <name>\` on an element:
102574
+ `);
102575
+ for (let index = 0; index < LOGO_NAMES.length; index += perLine)
102576
+ console.log(
102577
+ " " + LOGO_NAMES.slice(index, index + perLine).map((n) => n.padEnd(width)).join("").trimEnd()
102578
+ );
102579
+ console.log('\nAnything else: point at a file \u2014 `logo: "./logos/name.svg"`.');
102580
+ } else if (command === "themes") {
102581
+ console.log(
102582
+ `${themeNames.length} built-in themes \u2014 \`--theme <name>\`, or \`style { theme: \u2026 }\`:
102583
+ `
102584
+ );
102585
+ for (const name of themeNames) console.log(` ${name}`);
102586
+ console.log(
102587
+ '\nFor your own colours, pass a JSON file instead \u2014 `--theme ./my-theme.json`.\nIt extends a built-in and overrides only what it names:\n\n { "extends": "dark", "pal": { "bg": "#0d1117" } }\n'
102588
+ );
101411
102589
  } else if (command === "explain") {
101412
102590
  const code = args[1];
101413
102591
  if (!code) usage();