Ferrigura / Diagrams
Use case diagram
A use case diagram draws actors, use cases, system boundaries, notes and JSON tables, and the relationships between them (associations with arrow ends, include, extend and generalization). It starts with usecase-beta; the detector accepts the keyword when whitespace or the end of the text follows it. ferrigura parses it with a hand-written scanner and recursive-descent parser in ferrigura-syntax (src/use_case), and draws it on the unified renderer in ferrigura-render (src/use_case, with its own shapes in src/unified/shapes/usecase_*.rs), laid out by ELK Layered (0012). The unified renderer's shared machinery (the shapes registry, labels, clusters, edges, markers, the ELK glue) is described in flowchart.md; this page covers what is the use case diagram's own.
Model
The canonical model is the DB's state after the parse (usecaseDb.ts), as UseCaseDiagram in ferrigura-model (src/use_case.rs). The conventions of canonical-model.md apply; these are the fields. Every list is in the DB's order, and an optional key that the reference leaves undefined is absent from the JSON.
Scroll sideways for more columns.
| Field | Holds |
|---|---|
title | Always "" |
direction | TB, BT, RL or LR (the default); TD is stored as TB, and the last direction statement wins |
accTitle | The accessibility title, sanitized and trimmed |
accDescription | The accessibility description, sanitized |
actors | { id, label, labelType, type, icon?, business, stereotype?, parentId?, classes, styles }; type is normal, hollow, awesome or icon (an icon metadata makes it icon) |
useCases | { id, label, labelType, shape, business, stereotype?, parentId?, classes, styles }; shape is ellipse, or rect for the bracket form Id[Label] |
systemBoundaries | { id, label, labelType, type, members, classes, styles }; type is rect or package, and members are the ids of its elements in declaration order |
relationships | { id, explicitId, source, target, type, arrowType, label?, labelType?, minlen, classes, styles, animate, animation? } |
notes | { id, target, label, labelType }, with ids note-0, note-1 and so on |
jsonNodes | { id, value, propertyOrder, classes, styles, rows } |
classDefs | { id, styles }, the last classDef of an id winning |
ast | The graph AST of the parse (usecaseAst.ts): header, nodes, edges, groups, class definitions and one statement per line, with spans |
Details of the relationship and the table fields:
typeof a relationship isassociation,include,extendorgeneralization.arrowTypeis the DB's number: 0 solid arrow, 1 back arrow, 2 plain line, 3 circle, 4 cross, 5 reversed circle, 6 reversed cross.minlenis the number of dashes minus one. An anonymous edge isedge-0,edge-1and so on in source order, andexplicitIdtells anid@from those.animationisfastorslow;animate: truealone setsanimateand leavesanimationabsent.valueof a JSON table is the object asJSON.parsereads it, a non-finite number written as its sentinel string.propertyOrderlists the keys of each object in the order of the text, keyed by JSON pointer (""is the root), because a JavaScript object does not keep that order.rowsare the display rows ofgetData:{ key, accessibleKey, value }, the only place that shows how JavaScript writes the numbers (String(number)) and in which order the keys appear.astspans are[start, end]in UTF-16 code units, as JavaScript counts them. The AST drops the key__proto__fromnodes,groupsandclassDefs, and leaves out keys that the reference adds only for a truthy text (an emptyicon,stereotypeorlabel); thelabelof an actor, use case or boundary is left out when it equals the id.
What the model keeps from the DB's behavior:
- Labels are raw. The DB sanitizes labels in
getDataonly, so the model holds them as the grammar gave them, with the entity placeholders ofencodeEntities(fl°…¶ß). The header texts are sanitized, twice, as the reference does (once in the visitor's setter, once atcommit):accTitleloses its leading blanks andaccDescrits blanks after each line break. - The title is
"".Diagram.fromTextsets the front matter title, then the parser callsclear, which resets it. The grammar has notitlestatement. - An id from a quoted name is
label.replace(/\W/g, '_'), so"Café"isCaf_and"Hello World!"isHello_World_; two labels can collide, and the builder reports it. - A reference to an undeclared id creates a use case with the
ellipseshape. - Order of elements. Actors, use cases, boundaries and JSON tables are sorted by the offset of the first mention of their id (
reorderElements), a relationship endpoint included, not by the offset of their declaration.
The model leaves out the DB's symbol table and counters, the configuration, and the render input (getData). The renderer builds the latter from the model.
Parsing
prepare, remove_directives, cleanup_comments (whole-line %% lines and blank lines before them) and encode_entities run as Diagram.fromText runs them, and the text gets a final line feed. The syntax is line oriented: the header ends its line, and every statement ends with a line break or the end of the text.
The reference's parser is Chevrotain's (0007). The pipeline in ferrigura-syntax/src/use_case/ follows its stages:
scanner.rsis the lexer, in three modes: the default mode, the JSON body afterjson <id> @, and the stereotype after<<. Chevrotain tries the patterns of a mode in the written order and takes the first that matches, not the longest, except that a keyword'slonger_alt(Identifier) wins when it matches more. The scanner keeps that table and its custom matchers (comments, accessibility lines and blocks, Markdown strings, JSON objects, stereotype text) by hand.grammar.rsis recursive descent with the reference's choices. Where Chevrotain decides by its lookahead or a GATE, the same test runs in the same order: the order of thestatementalternatives, the OPTION guards ofrelationTailandentityName, thehasLabeledRightlook-ahead of the arrows, andLA(0), the exact image of the last token consumed.visitor.rsturns the syntax tree into the builder's drafts (draft.rs) and the AST statements, with UTF-16 spans.builder.rsportsusecaseModelBuilder.ts:finalizeruns the reference's steps in their order and checks the whole diagram, and the first failing check decides the error. A diagram is accepted or rejected as a whole.json.rsreads a JSON table's object asJSON.parsereads it, together with the order of its keys, in one pass; it also holdsjs_number_to_string(ECMA-262's Number-to-String) andjson_rows.ast.rsbuilds the graph AST.
Grammar quirks of the reference that ferrigura keeps on purpose, each probed against it:
- Keywords are not names.
actor,end,note,for,class,classDef,style,json,direction,systemBoundary,include,extend,true,false,TD,TB,BT,LR,RLandusecase-betacannot be ids. They are label text and style values; quoted names are the way out.includeandextendalone are case-insensitive. The direction keywords are upper case. x--ando--are operators wherever they start a token, sox-->yfails, andx --> yparses.--oand--xtake the start of any id that begins withoorx, soA --oz Bfails.- A label needs the exact image.
A <-- label -- BandA -- label --> Bare labeled;<---and---cannot take a label. The closing operator must match the family:<--,o--andx--close with--;--closes with-->,--,--oor--x. The closer must stand on the same line. metadataAssignmentStatementshadows the entity statement when@{follows a target, soA @{ type: "hollow" } --> Bis an error; the assignment stands on its own line. An edge's metadata is the statemente1@{ animate: true }.- Metadata values are identifiers, quoted strings,
trueorfalse. A number (1.5) is an error, a keyword (type: actor) is an error, and1mgis an identifier. - Boundaries hold entity names and
actorlines only. There are no relations, notes, styles or nested boundaries inside one.allowmixing,newpage,package,rectangleandskinparamare forbidden as the first word of a statement (PlantUML words). - Label text is the source slice, with its inner spacing. The scanner's last-resort token takes any run of non-printable-ASCII characters, so a non-ASCII letter is label text but a syntax error in a name; an id is ASCII
\w. - A comment is
%%as the first token of its line;A %% cis an error, and so is the header followed by one. The accessibility lines need a line start, indented or not. - A stereotype is non-blank text between
<<and>>on one line. - The dead alternative of
CssIdentifier(--…) is dropped:--is matched first.foo-baris a CSS identifier. - Only the first lexer error and the first parser error are reported, by position.
Layout and rendering
ferrigura-render/src/use_case/ ports usecaseDb.ts's getData, usecaseRenderer.ts's draw and styles.ts; ferrigura-render/src/unified/shapes/usecase_*.rs ports the shapes.
data.rsbuilds the unified layout data from the model, in the reference's order: actors, use cases, notes, JSON tables and boundaries, then the relationships, then one dotted edge per note. A node's color index counts actors and use cases together; boundaries count on their own, and notes and JSON tables do not count. It also holdsprepareUsecaseLayoutData's steps: plain labels are escaped (&,<,>), JSON cells are escaped, and the stereotype of an ellipse or a rectangle is folded into the label. It makes the accessible names (use case Sign in,Note for X: …,association label from A to B,package system boundary …) and the DOM ids (usecase-<id>,usecase-<diagram>-<edge>).mod.rsruns the steps ofdraw: layout data, the measure step, the data attributes ofannotateUsecaseElements(data-usecase-id,data-usecase-kind,role,aria-label), the layout, the paint, the font subsets, the root with itsviewBox(the drawn box plusdiagramPaddingon each side) and the fonts as custom properties on the root.style.rsports the stylesheet (styles.ts): the role colors (each of the eightusecase*theme variables with its fallbacks), the palette rules ofgenColor, and the rules of actors, use cases, boundaries, notes, JSON tables, relationships and markers. Colors are written asvar(--name, value); undercolorScheme: autothe rules of the dark set come after the light ones in a media query (0011).- Shapes.
usecaseActorand its three variants (figure of 56 by 72, label under it, stereotype above the label, the business marker as a chord at 60 degrees, an invisible outline that sizes the node),usecaseEllipse,usecaseBusiness(the ellipse around label and stereotype, with the marker),usecaseJsonTableand the boundaryusecaseSystemBoundary(clusters.js: a rectangle with its title at the top, a tab fortype: package). A[Rect]use case is the sharedrectshape, and a note the sharednoteshape. - Layout. The use case diagram uses ELK Layered through the shared glue. Ellipses set
spreadPorts, which gives themelk.portAlignment.default: CENTER. Boundaries are ELK subgraphs. - Markers. The point, circle and cross markers are the flowchart's under the type's own name; the extension marker of a generalization is the type's own,
<svgId>_usecase-extension…, with a-marginvariant for the neo look.
The sections of the usecase configuration that the renderer reads are wrappingWidth, minNodeWidth, the six font settings, diagramPadding, useMaxWidth and colorScheme. The defaults of the section are look: neo and theme: redux-color, which the configuration layers resolve (0008).
Differences from Mermaid
- Labels are SVG text, not HTML in
foreignObject(0009). The folded stereotype's first line gets itsusecase-stereotypeclass directly, where the reference adds it with a DOM query. - The layout is always ELK Layered (0012). The reference accepts
layout:with any registered algorithm and falls back to ELK for an unknown one; ferrigura lays out any other value with Layered as well. - Only the two redux themes ship (0011). A fixture that names another theme is drawn with the one it falls back to.
look: handDrawndraws the plain neo or classic elements. roughjs is not ported, and theisHandDrawnsplits of the stylesheet are not ported either.- Error messages are ferrigura's own (0007): a rejected text is an error at a position, and the differential run compares acceptance, not the text. The specs assert that a text fails and where.
- No icon packs. An actor with an
iconmetadata draws the reference's fallback symbol, which carries theusecase-actor-icon-fallbackclass. - JSON tables. A lone surrogate written as a
\uescape becomes U+FFFD, since a RustStringholds scalar values only; objects and arrays nested deeper than 512 levels are an error.
Known gaps
look: handDrawnand icon packs (above).usecase.nodeSpacingandusecase.rankSpacingare not read by the renderer: ELK takes the unified renderer's spacing. The geometry run is within tolerance on the fixture that sets both (10-configuration-and-accessibility), but the options have no effect.- Layout data has no differential run of its own:
getData's output is checked by Rust specs against the reference's spec expectations, and by the geometry run through the SVG, not field by field against the reference (there is nousecase-datarun). - The type has fixtures but no probes under
tools/port/geometry/probes.
Checking it
pnpm -C tools/port port:diff usecasecompares the canonical model, the AST and the JSON rows on every input: the fixtures, the string literals of Mermaid's specs, the documentation examples and the cases oftools/port/cases.ts(the grammar's probes, one input each). There are no exceptions.pnpm -C tools/port port:diff configincludes theusecasesection's defaults.pnpm -C tools/port port:geometry usecaserenders the fixtures ine2e/diagrams/usecasewith both and compares node positions and sizes, edge points and label boxes within 0.05 px. Thelook: handDrawnfixture is left out by the harness's rule. Colors are not compared, and the fixtures that name a theme ferrigura does not ship (themed-elements-base-theme,-dark-theme,-default-theme,-forest-theme,-neutral-theme) are rendered with the theme they fall back to. Four fixtures write their text with HTML entities (<<Employee>>); the harness decodes them (decodeFixtureEntities) before both sides parse.- The specs of
ferrigura-syntax, ported from Mermaid's:use_case_lexer_spec,use_case_parser_spec,use_case_publication_spec,use_case_spec,use_case_db_spec,use_case_json_specanduse_case_docs_spec, which parses the examples of the syntax page (tests/data/usecase.md). Those the publicparsecannot isolate are ignored with their reason: the grammar-level tests whose text the model builder rejects, and the one that reads the raw Chevrotain statements. - The specs of
ferrigura-render:use_case_data_spec(the layout data and the color slots),use_case_render_spec(the stylesheet and the role colors) anduse_case_quirks_spec(one test per quirk of the reference's renderer, below). - The snapshots (
crates/ferrigura/tests/snapshots/svg.tsv) pin the output of every fixture.
Learnings
The grammar's rules are in Parsing. These are the ones that the renderer and the harness add.
- A label is measured at the theme's font, and drawn at the diagram's.
labelHelpermeasures the label at the theme's font size (14 px forredux-color) before the shape adds its class, so the ellipse and the figure are sized by that box, and the actor's and business use case's labels are drawn afterwards atactorFontSizeandusecaseFontSize. The font families of the section are written into custom properties only; they never change the measured width (which is why a fixture withusecaseFontFamily: Georgiastill compares).NodeFields::drawn_font_sizecarries the drawn size. - An edge label is 10 px in a 14 px box. The stylesheet sets
.edgeLabelto 10 px, but its group is the box measured at 14 px plus 2 px on each side. The text starts at minus half of its drawn width, which is the measured width scaled to 10 px; the measured box is float32, so the drawn width agrees with the reference to about a hundredth of a pixel. - The stereotype fold differs by shape. An ellipse or a rectangle gets
«stereotype»<br/>labelas a Markdown label, the Markdown markers (* [ ] \ _and the backquote) backslash-escaped in both parts. Only the ellipse's first line takesusecase-stereotype(the reference's captures give a folded rectangle none). An actor and a business ellipse keep the stereotype as a label of their own, above the figure or the label. - Escaping runs after sanitizing, and the names run before escaping.
getDatasanitizes a label, thendrawescapes a plain label (&,<,>) while the accessible name is made from the sanitized label. DOMPurify returns a text without<unchanged, so&stays, and a text with<is serialized, so<becomes<before the escape makes it&lt;. - A Markdown label is not centered by half its width. Labels are start-anchored at the origin, so
createTextdoes not shift the group left; the shape centers the box withtranslate(-w/2 - x, …). - An empty diagram has the padding around nothing:
viewBox="-20 -20 40 40"andmax-width: 40px. - A property of a class keeps the place of its first write and the value of its last.
getCompiledStylesmerges thedefaultclass and the node's classes by property, and a JSON table's duplicate key keeps its first position and its last value, asJSON.parseand the reference's key walk do. - JavaScript's number text. A JSON cell shows
String(value):Infinity,0for-0, the shortest digits that read back (an exact tie takes the even digit), exponent form outside 1e-6 to 1e21, and an integer above 2^53 rounded (12345678901234567890is12345678901234567000).serde_json's default text is not that, sojson.rsformats the number itself. - The business marker's chord is computed with V8's
cosandsinofπ/3(ferrigura-jsmath) and a plainsqrt, which is exact. - Entities.
#35;-style entities reach the model as placeholders and are decoded at render time, in the label path. The reference's e2e viewer decodes entities before parsing, which is why the geometry harness does so for the fixtures that write them.