archsight 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +4 -0
  3. data/docs/configuration.md +5 -0
  4. data/docs/diagram.md +5 -0
  5. data/docs/index.md.erb +1 -0
  6. data/docs/pdf-export.md +118 -0
  7. data/lib/archsight/analysis/typst_renderer.rb +25 -78
  8. data/lib/archsight/cli.rb +37 -0
  9. data/lib/archsight/diagram/graph/attributes.rb +3 -1
  10. data/lib/archsight/diagram/graph/node.rb +18 -1
  11. data/lib/archsight/diagram/layout/expander.rb +75 -3
  12. data/lib/archsight/diagram/layout/rank_arranger.rb +3 -1
  13. data/lib/archsight/markdown.rb +17 -1
  14. data/lib/archsight/pdf/cache.rb +41 -0
  15. data/lib/archsight/pdf/compiler.rb +69 -0
  16. data/lib/archsight/pdf/dependency_graph.rb +127 -0
  17. data/lib/archsight/pdf/document.rb +227 -0
  18. data/lib/archsight/pdf/embeds.rb +134 -0
  19. data/lib/archsight/pdf/images.rb +94 -0
  20. data/lib/archsight/pdf/markdown.rb +217 -0
  21. data/lib/archsight/pdf/remote_image.rb +122 -0
  22. data/lib/archsight/pdf/resource_sheet.rb +129 -0
  23. data/lib/archsight/pdf/sample_document.rb +67 -0
  24. data/lib/archsight/pdf/tables.rb +72 -0
  25. data/lib/archsight/pdf/theme.rb +125 -0
  26. data/lib/archsight/pdf/typst/archsight.typ +152 -0
  27. data/lib/archsight/pdf/typst.rb +45 -0
  28. data/lib/archsight/pdf.rb +30 -0
  29. data/lib/archsight/resources/config.rb +147 -0
  30. data/lib/archsight/version.rb +1 -1
  31. data/lib/archsight/web/api/openapi/spec.yaml +230 -0
  32. data/lib/archsight/web/api/pdf_helpers.rb +46 -0
  33. data/lib/archsight/web/api/routes.rb +30 -0
  34. data/lib/archsight/web/public/vue/DocPage-D3QtnrmO.js +1 -0
  35. data/lib/archsight/web/public/vue/EditorPage-lZyG6dVt.js +35 -0
  36. data/lib/archsight/web/public/vue/ErrorPage-BhdYGZJk.js +2 -0
  37. data/lib/archsight/web/public/vue/GraphView-CoqAjFdR.js +1 -0
  38. data/lib/archsight/web/public/vue/HomePage-B7SaXVeb.js +2 -0
  39. data/lib/archsight/web/public/vue/{InstanceRouter-CJSkGdBG.css → InstanceRouter-BCKbW-Wp.css} +1 -1
  40. data/lib/archsight/web/public/vue/InstanceRouter-DJJn9fhP.js +1 -0
  41. data/lib/archsight/web/public/vue/KindList-UqMajqbf.js +1 -0
  42. data/lib/archsight/web/public/vue/PageView-CirIXjW6.js +1 -0
  43. data/lib/archsight/web/public/vue/QueryError-BIJH6gQC.js +1 -0
  44. data/lib/archsight/web/public/vue/ResourceList-761vG1Jw.js +2 -0
  45. data/lib/archsight/web/public/vue/SearchResults-BPHa6yhK.js +1 -0
  46. data/lib/archsight/web/public/vue/WikiPage-Bi7dzY2h.js +3 -0
  47. data/lib/archsight/web/public/vue/{WikiPage-TpmM1VAF.css → WikiPage-CuUi4C8v.css} +1 -1
  48. data/lib/archsight/web/public/vue/{index-DMhBow5t.js → index-BF4zUlWZ.js} +2 -2
  49. data/lib/archsight/web/public/vue/index-BO59Fbpf.css +1 -0
  50. data/lib/archsight/web/public/vue.html +2 -2
  51. metadata +35 -17
  52. data/lib/archsight/web/public/vue/DocPage-L0GzDFiV.js +0 -1
  53. data/lib/archsight/web/public/vue/EditorPage-B3iNbh1u.js +0 -35
  54. data/lib/archsight/web/public/vue/ErrorPage-Lro5vwhy.js +0 -2
  55. data/lib/archsight/web/public/vue/GraphView-BRKwxa--.js +0 -1
  56. data/lib/archsight/web/public/vue/HomePage-DKgI1DXr.js +0 -2
  57. data/lib/archsight/web/public/vue/InstanceRouter-CX6qxVTm.js +0 -1
  58. data/lib/archsight/web/public/vue/KindList-C556gf23.js +0 -1
  59. data/lib/archsight/web/public/vue/PageView-DAqHjL1x.js +0 -1
  60. data/lib/archsight/web/public/vue/QueryError-DvspMrsh.js +0 -1
  61. data/lib/archsight/web/public/vue/ResourceList-BZOYa68X.js +0 -2
  62. data/lib/archsight/web/public/vue/SearchResults-DaaOPG3t.js +0 -1
  63. data/lib/archsight/web/public/vue/WikiPage-Bfx92Iig.js +0 -3
  64. data/lib/archsight/web/public/vue/index-B_YGrWD8.css +0 -1
@@ -374,6 +374,234 @@ paths:
374
374
  "404":
375
375
  description: Unknown colour
376
376
 
377
+ "/api/v1/kinds/Config/instances/{name}/preview":
378
+ get:
379
+ summary: Preview the look of a PDF Config
380
+ description: A fixed sample document (cover page and a page with headings, list,
381
+ quote, table with status badges and code) in the look of one `Config` of type
382
+ `pdf`, typeset by Typst like every PDF export. Served inline, for showing it
383
+ in a page. Only the Config shapes the result, so a changed colour, font or logo
384
+ is visible at once. The response carries an `ETag` (fingerprint of the document
385
+ and the files it is made of) and answers `304` for a matching `If-None-Match`.
386
+ operationId: getConfigPreview
387
+ tags:
388
+ - PDF
389
+ parameters:
390
+ - name: name
391
+ in: path
392
+ required: true
393
+ description: Name of the Config, e.g. `Config:PDF:default`
394
+ schema:
395
+ type: string
396
+ - name: part
397
+ in: query
398
+ required: false
399
+ description: "`cover` or `page` for a document of just that page (the viewer needs no navigation); without it both pages"
400
+ schema:
401
+ type: string
402
+ enum:
403
+ - cover
404
+ - page
405
+ - name: If-None-Match
406
+ in: header
407
+ required: false
408
+ description: The `ETag` of a copy the client has; a match answers `304`
409
+ schema:
410
+ type: string
411
+ responses:
412
+ '200':
413
+ description: The PDF
414
+ headers:
415
+ ETag:
416
+ schema:
417
+ type: string
418
+ Content-Disposition:
419
+ description: '`inline; filename="preview-<name>.pdf"`'
420
+ schema:
421
+ type: string
422
+ content:
423
+ application/pdf:
424
+ schema:
425
+ type: string
426
+ format: binary
427
+ '304':
428
+ description: The client's copy is current
429
+ '404':
430
+ description: No such Config
431
+ content:
432
+ application/json:
433
+ schema:
434
+ "$ref": "#/components/schemas/Error"
435
+ '422':
436
+ description: The Config is not of type `pdf`, or the sample could not be typeset
437
+ (message of typst)
438
+ content:
439
+ application/json:
440
+ schema:
441
+ "$ref": "#/components/schemas/Error"
442
+ '503':
443
+ description: typst is not installed
444
+ content:
445
+ application/json:
446
+ schema:
447
+ "$ref": "#/components/schemas/Error"
448
+
449
+ "/api/v1/kinds/{kind}/instances/{name}/pdf":
450
+ get:
451
+ summary: Download a resource as PDF
452
+ description: |
453
+ Returns one resource as a PDF document, laid out by Typst (`typst` must be installed, the container image has it).
454
+
455
+ **What it contains**: a page is exported as written (headings, tables, images, `asd` diagrams as vector graphics,
456
+ `[[links]]` as their label); any other resource becomes a generated document with its description,
457
+ properties, relations and its `architecture/diagram`. Tags steer the layout: `pdf:cover`, `pdf:toc`,
458
+ `pdf:landscape`, `pdf:draft`, `pdf:template:<name>` and `version:<x>`.
459
+
460
+ **Look**: colours, fonts, logo and the texts of header and footer come from the `Config` resource of type `pdf` named
461
+ `default` (or the one `template` / the tag names); without one a neutral look is used.
462
+
463
+ The response carries an `ETag` (a fingerprint of everything the PDF is made from, so a changed page, image,
464
+ diagram or template changes it) and answers `304` for a matching `If-None-Match`.
465
+ operationId: getInstancePdf
466
+ tags:
467
+ - PDF
468
+ parameters:
469
+ - "$ref": "#/components/parameters/kind"
470
+ - "$ref": "#/components/parameters/name"
471
+ - name: template
472
+ in: query
473
+ required: false
474
+ description: Name of a `Config` of type `pdf` to use instead of the tag or `default`
475
+ schema:
476
+ type: string
477
+ - name: draft
478
+ in: query
479
+ required: false
480
+ description: "`1` adds the draft watermark"
481
+ schema:
482
+ type: string
483
+ enum:
484
+ - '1'
485
+ - name: If-None-Match
486
+ in: header
487
+ required: false
488
+ description: The `ETag` of a copy the client has; a match answers `304`
489
+ schema:
490
+ type: string
491
+ responses:
492
+ '200':
493
+ description: The PDF
494
+ headers:
495
+ ETag:
496
+ schema:
497
+ type: string
498
+ Content-Disposition:
499
+ description: '`attachment; filename="<name>.pdf"`'
500
+ schema:
501
+ type: string
502
+ content:
503
+ application/pdf:
504
+ schema:
505
+ type: string
506
+ format: binary
507
+ '304':
508
+ description: The client's copy is current
509
+ '404':
510
+ description: Not found
511
+ content:
512
+ application/json:
513
+ schema:
514
+ "$ref": "#/components/schemas/Error"
515
+ '422':
516
+ description: The document could not be typeset (message of typst)
517
+ content:
518
+ application/json:
519
+ schema:
520
+ "$ref": "#/components/schemas/Error"
521
+ '503':
522
+ description: typst is not installed
523
+ content:
524
+ application/json:
525
+ schema:
526
+ "$ref": "#/components/schemas/Error"
527
+ "/api/v1/pages/{name}/pdf":
528
+ get:
529
+ summary: Download a page as PDF
530
+ description: |
531
+ Returns one page as a PDF document, laid out by Typst (`typst` must be installed, the container image has it).
532
+
533
+ **What it contains**: a page is exported as written (headings, tables, images, `asd` diagrams as vector graphics,
534
+ `[[links]]` as their label); any other resource becomes a generated document with its description,
535
+ properties, relations and its `architecture/diagram`. Tags steer the layout: `pdf:cover`, `pdf:toc`,
536
+ `pdf:landscape`, `pdf:draft`, `pdf:template:<name>` and `version:<x>`.
537
+
538
+ **Look**: colours, fonts, logo and the texts of header and footer come from the `Config` resource of type `pdf` named
539
+ `default` (or the one `template` / the tag names); without one a neutral look is used.
540
+
541
+ The response carries an `ETag` (a fingerprint of everything the PDF is made from, so a changed page, image,
542
+ diagram or template changes it) and answers `304` for a matching `If-None-Match`.
543
+ operationId: getPagePdf
544
+ tags:
545
+ - PDF
546
+ parameters:
547
+ - "$ref": "#/components/parameters/name"
548
+ - name: template
549
+ in: query
550
+ required: false
551
+ description: Name of a `Config` of type `pdf` to use instead of the tag or `default`
552
+ schema:
553
+ type: string
554
+ - name: draft
555
+ in: query
556
+ required: false
557
+ description: "`1` adds the draft watermark"
558
+ schema:
559
+ type: string
560
+ enum:
561
+ - '1'
562
+ - name: If-None-Match
563
+ in: header
564
+ required: false
565
+ description: The `ETag` of a copy the client has; a match answers `304`
566
+ schema:
567
+ type: string
568
+ responses:
569
+ '200':
570
+ description: The PDF
571
+ headers:
572
+ ETag:
573
+ schema:
574
+ type: string
575
+ Content-Disposition:
576
+ description: '`attachment; filename="<name>.pdf"`'
577
+ schema:
578
+ type: string
579
+ content:
580
+ application/pdf:
581
+ schema:
582
+ type: string
583
+ format: binary
584
+ '304':
585
+ description: The client's copy is current
586
+ '404':
587
+ description: Not found
588
+ content:
589
+ application/json:
590
+ schema:
591
+ "$ref": "#/components/schemas/Error"
592
+ '422':
593
+ description: The document could not be typeset (message of typst)
594
+ content:
595
+ application/json:
596
+ schema:
597
+ "$ref": "#/components/schemas/Error"
598
+ '503':
599
+ description: typst is not installed
600
+ content:
601
+ application/json:
602
+ schema:
603
+ "$ref": "#/components/schemas/Error"
604
+
377
605
  /api/v1/assets/{path}:
378
606
  get:
379
607
  summary: Get an asset (image or draw.io diagram)
@@ -2120,6 +2348,8 @@ tags:
2120
2348
  description: Search and query operations
2121
2349
  - name: Pages
2122
2350
  description: Wiki pages (markdown with frontmatter), the page tree and the home page
2351
+ - name: PDF
2352
+ description: Pages and resources as PDF documents (Typst)
2123
2353
  - name: Assets
2124
2354
  description: Images and draw.io diagrams that markdown embeds
2125
2355
  - name: Macros
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../pdf"
4
+ require_relative "../../pdf/cache"
5
+ require_relative "../../pdf/sample_document"
6
+
7
+ module Archsight; end
8
+ module Archsight::Web; end
9
+ module Archsight::Web::API; end
10
+
11
+ # Download of a page or resource as PDF
12
+ module Archsight::Web::API::PdfHelpers
13
+ CACHE = Archsight::Pdf::Cache.new
14
+ COMPILER = Archsight::Pdf::Compiler.new
15
+
16
+ # Sends the PDF of `resource`. The ETag is the fingerprint of everything the PDF is made from, so a client (and
17
+ # the server's cache) keeps it exactly as long as the page, its images, diagrams and the template are unchanged.
18
+ # Query: `template` (name of a pdf Config), `draft=1` (watermark).
19
+ def serve_pdf(resource)
20
+ pdf_response do
21
+ Archsight::Pdf.document(db, resource, compiler: COMPILER, template: params[:template].to_s.empty? ? nil : params[:template],
22
+ draft: params[:draft] == "1")
23
+ end
24
+ end
25
+
26
+ # Sends the sample document in the look of a pdf Config, shown in the page instead of downloaded
27
+ def serve_pdf_preview(config)
28
+ pdf_response("inline") { Archsight::Pdf::SampleDocument.new(db, config, part: params[:part], pdf_images: COMPILER.pdf_images?) }
29
+ end
30
+
31
+ # Typesets the document the block builds and sends it; typst problems are answered as JSON errors
32
+ def pdf_response(disposition = "attachment")
33
+ document = yield
34
+ headers "X-Content-Type-Options" => "nosniff"
35
+ cache_control :private, :must_revalidate, max_age: 0
36
+ etag document.fingerprint
37
+ pdf = CACHE.fetch(document.fingerprint) { Archsight::Pdf.compile(document, compiler: COMPILER) }
38
+ content_type "application/pdf"
39
+ headers "Content-Disposition" => %(#{disposition}; filename="#{document.filename}")
40
+ pdf
41
+ rescue Archsight::Pdf::Unavailable => e
42
+ json_error(e.message, status: 503, error_type: "ServiceUnavailable")
43
+ rescue Archsight::Pdf::Error => e
44
+ json_error(e.message, status: 422, error_type: "UnprocessableEntity")
45
+ end
46
+ end
@@ -6,6 +6,7 @@ require_relative "json_helpers"
6
6
  require_relative "page_helpers"
7
7
  require_relative "requirements_helpers"
8
8
  require_relative "analysis_helpers"
9
+ require_relative "pdf_helpers"
9
10
  require_relative "../../assets"
10
11
  require_relative "../../openapi"
11
12
 
@@ -21,6 +22,7 @@ module Archsight::Web::API::Routes
21
22
  helpers Archsight::Web::API::PageHelpers
22
23
  helpers Archsight::Web::API::RequirementsHelpers
23
24
  helpers Archsight::Web::API::AnalysisHelpers
25
+ helpers Archsight::Web::API::PdfHelpers
24
26
 
25
27
  # Rendering is CPU work, previews are typed, not pasted
26
28
  MAX_DIAGRAM_SOURCE = 100_000
@@ -79,6 +81,26 @@ module Archsight::Web::API::Routes
79
81
  .merge(graph_layers: Archsight::GraphvisRenderer.default_graph_layers(instance.class.layer)))
80
82
  end
81
83
 
84
+ # GET /api/v1/kinds/Config/instances/:name/preview - A sample document in the look of a pdf Config (inline PDF)
85
+ get "/api/v1/kinds/Config/instances/:name/preview" do
86
+ config = db.instance_by_kind("Config", params[:name]) || db.instance_by_alias("Config", params[:name])
87
+ json_error("Instance '#{params[:name]}' not found", status: 404, error_type: "NotFound") unless config
88
+ json_error("Config '#{config.name}' is not of type #{Archsight::Pdf::Theme::TYPE}", status: 422, error_type: "UnprocessableEntity") unless config.annotations["config/type"].to_s.casecmp?(Archsight::Pdf::Theme::TYPE)
89
+
90
+ serve_pdf_preview(config)
91
+ end
92
+
93
+ # GET /api/v1/kinds/:kind/instances/:name/pdf - The resource as PDF document (Typst), see Archsight::Pdf
94
+ get "/api/v1/kinds/:kind/instances/:name/pdf" do
95
+ klass = Archsight::Resources[params[:kind]]
96
+ json_error("Kind '#{params[:kind]}' not found", status: 404, error_type: "NotFound") unless klass
97
+
98
+ instance = db.instance_by_kind(params[:kind], params[:name]) || db.instance_by_alias(params[:kind], params[:name])
99
+ json_error("Instance '#{params[:name]}' not found", status: 404, error_type: "NotFound") unless instance
100
+
101
+ serve_pdf(instance)
102
+ end
103
+
82
104
  # GET /api/v1/pages - Page tree built from PageMenu resources, page tags and filters and the home page
83
105
  # (shown at `/`; it is not in the tree unless a menu contains it)
84
106
  get "/api/v1/pages" do
@@ -95,6 +117,14 @@ module Archsight::Web::API::Routes
95
117
  json_response(build_page_response(page))
96
118
  end
97
119
 
120
+ # GET /api/v1/pages/:name/pdf - The page as PDF document (Typst), see Archsight::Pdf
121
+ get "/api/v1/pages/:name/pdf" do
122
+ page = db.instance_by_kind("Page", params[:name])
123
+ json_error("Page '#{params[:name]}' not found", status: 404, error_type: "NotFound") unless page
124
+
125
+ serve_pdf(page)
126
+ end
127
+
98
128
  # GET /api/v1/search - Search with query language
99
129
  get "/api/v1/search" do
100
130
  query = params[:q]
@@ -0,0 +1 @@
1
+ import{B as e,L as t,R as n,X as r,it as i,p as a,t as o,tt as s}from"./index-BF4zUlWZ.js";import{t as c}from"./WikiPage-Bi7dzY2h.js";var l={key:1},u={__name:`DocPage`,props:{filename:String},setup(u){let d=u,f=i(null);function p(e,t){let n=new DOMParser().parseFromString(e,`text/html`).body,r=n.querySelector(`:scope > article`)||n,i=r.querySelector(`h1`),a=i?.textContent.trim()||t.split(`/`).pop().replace(/[-_]/g,` `);i?.remove();let o=[...r.querySelectorAll(`h2[id], h3[id], h4[id]`)].map(e=>({level:Number(e.tagName[1]),id:e.id,text:e.textContent.trim()})),s=[{title:`Documentation`}];return t.startsWith(`resources/`)&&s.push({title:`Resource types`}),{title:a,html:r.innerHTML,toc:o.length>=2?o:[],breadcrumb:s}}async function m(){let e=await a(d.filename);f.value=e?p(e,d.filename):{title:`Not found`,html:`<p>Documentation not found.</p>`,toc:[],breadcrumb:[{title:`Documentation`}]},o(f.value.title)}return s(()=>d.filename,m,{immediate:!0}),(i,a)=>f.value?(r(),n(c,{key:0,title:f.value.title,html:f.value.html,breadcrumb:f.value.breadcrumb,toc:f.value.toc},null,8,[`title`,`html`,`breadcrumb`,`toc`])):(r(),e(`article`,l,[...a[0]||=[t(`p`,null,`Loading...`,-1)]]))}};export{u as default};