Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/unreleased/render-source-links.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Link rendered diagram elements to their source.** PlantUML and DOT link nodes and edges, Mermaid links supported nodes and participants, and CLI, REPL, LSP and document renderings accept source-link templates.
7 changes: 6 additions & 1 deletion cmd/sysml/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ var (
renderAllDir string
renderForm string
renderPalette string
renderLink string
renderUnplaced string
renderStyle string
renderPorts string
Expand Down Expand Up @@ -384,6 +385,10 @@ func runCLI() int {
fmt.Fprintln(os.Stderr, "sysml: -render-palette is the palette -render or -render-all fills DOT, Mermaid or PlantUML with; name the view to render with -render or a directory with -render-all")
return 2
}
if renderLink != "" && renderView == "" && renderAllDir == "" && renderDoc == "" && renderDocsDir == "" {
fmt.Fprintln(os.Stderr, "sysml: -render-link links rendered elements to their source; name what to render with -render, -render-all, -render-document or -render-documents")
return 2
}
if renderPorts != "" && renderView == "" && renderAllDir == "" {
fmt.Fprintln(os.Stderr, "sysml: -render-ports is how much of a part's ports -render or -render-all draws on an interconnection; name the view to render with -render or a directory with -render-all")
return 2
Expand Down Expand Up @@ -538,7 +543,7 @@ func runCLI() int {
case convertFormat != "" || migrateFormat != "" || renderView != "" || renderDoc != "" || renderAllDir != "" || renderDocsDir != "" || queryText != "" || len(evalExprs) > 0:
fmt.Fprintf(os.Stderr, "sysml: %s syncs a change set; it cannot be combined with -convert, -migrate, -render, -render-all, -render-document, -render-documents, -query or -eval\n", mode)
return 2
case outputPath != "" || fromFormat != "" || renderForm != "" || renderPalette != "" || renderUnplaced != "" || renderStyle != "" || renderPorts != "" || docForm != "" || diagramForm != "" || pdfEngine != "" || pdfTitlePage || pdfTOC || pdfNumbering || docNumberFigures:
case outputPath != "" || fromFormat != "" || renderForm != "" || renderPalette != "" || renderLink != "" || renderUnplaced != "" || renderStyle != "" || renderPorts != "" || docForm != "" || diagramForm != "" || pdfEngine != "" || pdfTitlePage || pdfTOC || pdfNumbering || docNumberFigures:
fmt.Fprintf(os.Stderr, "sysml: %s reads SysML or Turtle inputs and reports the change set; -output, -from and the render options do not apply\n", mode)
return 2
case modelChecks.requested():
Expand Down
18 changes: 18 additions & 0 deletions cmd/sysml/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ func runRender(files []string) error {
if err != nil {
return err
}
if options.Links.Template != "" {
options.Links.Sites, err = sess.ViewSites()
if err != nil {
return err
}
}
artifact, err := rendering.WriteWith(form, options)
if err != nil {
return err
Expand Down Expand Up @@ -75,6 +81,12 @@ func runRenderAll(files []string) error {
if len(views) == 0 {
return errors.New("the model declares no views; nothing was rendered")
}
if options.Links.Template != "" {
options.Links.Sites, err = sess.ViewSites()
if err != nil {
return err
}
}
if err := os.MkdirAll(renderAllDir, 0o750); err != nil {
return fmt.Errorf("create rendering directory %s: %w", renderAllDir, err)
}
Expand Down Expand Up @@ -144,6 +156,12 @@ func renderFilenames(views []model.ViewInfo, form view.Form) (map[string]string,
// names, each of which must be one there is.
func renderOptions(width int) (view.Options, error) {
options := view.Options{Width: width}
if renderLink != "" {
if err := view.ParseLinkTemplate(renderLink); err != nil {
return view.Options{}, fmt.Errorf("-render-link: %w", err)
}
options.Links.Template = renderLink
}
if renderPalette != "" {
palette, ok := view.ParsePalette(renderPalette)
if !ok {
Expand Down
8 changes: 7 additions & 1 deletion cmd/sysml/render_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func documentOptions(outputDir string) docrender.HTMLOptions {
DiagramForm: view.Form(diagramForm),
Unplaced: view.Unplaced(renderUnplaced),
Style: view.DrawingStyle(renderStyle),
LinkTemplate: renderLink,
Drawer: replext.Drawer(),
WithoutGraphviz: replext.Drawer() == nil,
OutputDir: outputDir,
Expand All @@ -149,7 +150,7 @@ func documentOptions(outputDir string) docrender.HTMLOptions {
// outputDir, "" for standard output.
func markdownOptions(outputDir string) docrender.MarkdownOptions {
return docrender.MarkdownOptions{
DiagramForm: view.Form(diagramForm), Unplaced: view.Unplaced(renderUnplaced), Style: view.DrawingStyle(renderStyle),
DiagramForm: view.Form(diagramForm), Unplaced: view.Unplaced(renderUnplaced), Style: view.DrawingStyle(renderStyle), LinkTemplate: renderLink,
Drawer: replext.Drawer(), WithoutGraphviz: replext.Drawer() == nil,
OutputDir: outputDir, NumberFigures: docNumberFigures,
}
Expand All @@ -168,6 +169,11 @@ func artifactDir() string {
// -render-unplaced value naming no placement and a -render-style value naming
// no drawing style.
func checkDiagramForm() error {
if renderLink != "" {
if err := view.ParseLinkTemplate(renderLink); err != nil {
return fmt.Errorf("-render-link: %w", err)
}
}
if _, err := unplacedOption(); err != nil {
return err
}
Expand Down
15 changes: 15 additions & 0 deletions cmd/sysml/render_document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ func TestRenderDocumentDiagramForm(t *testing.T) {
2, "SomeView")
}

func TestRenderDocumentDiagramSourceLinks(t *testing.T) {
binary := buildCLI(t)
fixture := filepath.Join("..", "..", "internal", "doc", "docrender", "testdata", "telescope_report.sysml")
template := "https://example.test/src/{file}#L{line}"
cmd := exec.Command(binary, fixture, "-render-document", "Observatory::MassReport", "-diagram-form", "plantuml", "-render-link", template)
out, err := cmd.Output()
if err != nil {
t.Fatalf("render linked document: %v", err)
}
want := "https://example.test/src/" + filepath.ToSlash(fixture) + "#L"
if !strings.Contains(string(out), want) || !strings.Contains(string(out), "[[") {
t.Errorf("document diagrams lack source links to %q:\n%s", want, out)
}
}

// TestRenderDocumentCommittedFixture renders the renderer's committed fixture
// through the binary's full analysis, matching the committed golden Markdown.
func TestRenderDocumentCommittedFixture(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions cmd/sysml/render_pdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@ func pdfOptions() (docpdf.Options, error) {
DiagramForm: page.DiagramForm,
Unplaced: page.Unplaced,
Style: page.Style,
LinkTemplate: page.LinkTemplate,
}, nil
}
41 changes: 41 additions & 0 deletions cmd/sysml/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,47 @@ func TestRenderPalette(t *testing.T) {
}
}

func TestRenderLinkFlag(t *testing.T) {
binary := buildCLI(t)
dir := t.TempDir()
path := writeModel(t, dir, "linked.sysml", `package Demo {
part def Vehicle;
view overview { expose Demo::Vehicle; }
}
`)
template := "https://example.test/src/{file}#L{line}:{col}"
got := runFiles(t, binary, []string{path}, "-render", "Demo::overview", "-render-form", "mermaid", "-render-link", template)
if got.status != exitHolds {
t.Fatalf("exit status = %d, want %d\n%s", got.status, exitHolds, got.output())
}
want := `click n0 href "https://example.test/src/` + filepath.ToSlash(path) + `#L2:`
if !strings.Contains(got.stdout, want) {
t.Errorf("rendered artifact lacks the source link %q:\n%s", want, got.stdout)
}

allDir := filepath.Join(t.TempDir(), "all")
all := runFiles(t, binary, []string{path}, "-render-all", allDir, "-render-form", "mermaid", "-render-link", template)
if all.status != exitHolds {
t.Fatalf("-render-all status = %d, want %d\n%s", all.status, exitHolds, all.output())
}
artifact, err := os.ReadFile(filepath.Join(allDir, "Demo.overview.mmd"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(artifact), want) {
t.Errorf("-render-all artifact lacks source links:\n%s", artifact)
}

invalid := runFiles(t, binary, []string{path}, "-render", "Demo::overview", "-render-link", "https://example.test/{unknown}")
if invalid.status != exitUnevaluable || !strings.Contains(invalid.stderr, "-render-link: unknown link template placeholder {unknown}") || invalid.stdout != "" {
t.Errorf("invalid link template = %d\n%s", invalid.status, invalid.output())
}
alone := runStreams(t, binary, renderModel, "-render-link", template)
if alone.status != 2 || !strings.Contains(alone.stderr, "-render-link links rendered elements to their source") {
t.Errorf("link template without a render target = %d\n%s", alone.status, alone.output())
}
}

// TestRenderSeveralFiles checks that a view declared in one file renders the
// elements its sibling files declare, loaded as one model, on stdout and into
// -o in the form -render-form names.
Expand Down
2 changes: 2 additions & 0 deletions cmd/sysml/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ func registerFlags(fs *flag.FlagSet) {
fs.StringVar(&renderAllDir, "render-all", "", "Render every declared view into this directory")
fs.StringVar(&renderForm, "render-form", "", "Form -render or -render-all writes: text, mermaid, markdown, dot, plantuml, csv or tsv (csv and tsv for a table); default from the destination for -render, each kind's machine form for -render-all")
fs.StringVar(&renderPalette, "render-palette", "", "Palette the dot, mermaid or plantuml form fills nodes from, by keyword family: okabe-ito, tol-bright, tol-muted, tol-light, brewer-set2, brewer-dark2, viridis or cividis; default black and white")
fs.StringVar(&renderLink, "render-link", "", "Link template for rendered elements: {file} is the path as loaded; use absolute paths for vscode:// or file:// links. Placeholders: {file}, {line}, {col}, {qname}, {id}")
fs.StringVar(&renderStyle, "render-style", "", "Drawing style of the dot or mermaid form: pilot (default), the Pilot visualizer's black and white, or cameo, the look of Cameo Systems Modeler; applies to -render, -render-all and document diagrams")
fs.StringVar(&renderPorts, "render-ports", "", "How much of a part's ports -render or -render-all draws on an interconnection: minimal (default), the ports its connectors end at, each a small square on the part's border named beside it, or full, every port, labelled name : Type")
fs.StringVar(&renderUnplaced, "render-unplaced", "", "Where a graph form of a view some Layout positions puts the nodes none does: omit (default) leaves them undrawn in every form, strip draws them, in rows below the dot drawing; applies to -render, -render-all and document diagrams")
Expand Down Expand Up @@ -790,6 +791,7 @@ func optionGroups() []usage.OptionGroup {
usage.Opt("render-all", "<dir>"),
usage.Opt("render-form", formArg),
usage.Opt("render-palette", "<palette>"),
usage.Opt("render-link", "<template>"),
usage.Opt("render-unplaced", "<placement>"),
usage.Opt("render-style", "<style>"),
usage.Opt("render-ports", "<display>"),
Expand Down
9 changes: 9 additions & 0 deletions docs/project/html-document-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ is replaced by `<img>` with the caption as its `alt` text; that is the path the
use, since no print engine runs Mermaid. Table-kind views keep rendering as a table, as they do
in Markdown.

An optional source-link template is applied to the nodes and edges that can be located in the
diagram's source model. HTML documents do not create element-anchored sections, so links back into
the document are not available as a substitute for source links. This backend leaves Mermaid's
`securityLevel` unset. Mermaid CLI 11.16.0 defaults to `strict`, which strips links with
non-HTTP(S) schemes, including `vscode://` and `file:///`. It rewrites sequence hrefs under both
`strict` and `loose`: `https://example.com/c%5D%22%23#L3` becomes `https://example.com/c]%22#`,
losing its fragment. `{"securityLevel":"loose"}` preserves non-HTTP(S) schemes, but not the URL
rewriting or fragment loss.

Supplying the images stays out of `docrender`: rendering them means running `mmdc` as a
subprocess, which is `docpdf`'s job and must not become a dependency of a pure renderer. So
`docrender` exposes the diagram sources in document order and accepts the resulting image
Expand Down
Loading
Loading