Selena is a textual language that compiles to UML sequence diagrams. It is written in (mostly) object-oriented TypeScript.
This package contains all the fundamental logic for making the system work. This includes: a lexer (tokenizer), a parser, the sequence data structures, diagram data structures, diagram layout computation, renderer interface and renderer implementations. This package does not contain any user interface.
The design of this system is extremely flexible overall:
- Information flows one-way: input to tokenizer, tokenizer to parser, to diagram construction, to renderer.
- There are very few token types used in the custom-built lexer, and the parser is a recursive-descent parser that is simple to understand and reason about.
- The horizontal and vertical layout algorithms work independently of each other and can easily be swapped out.
- The horizontal layout algorithm is based on an abstract system of constraints that is, in my opinion, quite elegant.
- The renderer interface is rather minimal, so that a wide range of rendering targets could be supported without changing anything about the diagram structure.
Install with NPM:
npm install selena
The installed package contains the transpiled JavaScript sources and TypeScript typings, if you need them.
The Selena script is first compiled to a Sequence
object via the exported
function compileToSequence
.
This Sequence
can then be converted into a Diagram
, which can be rendered
to any target.
When rendering, the diagram needs to be laid out, then its size needs to be
measured and the renderer prepared for that size, at which point the diagram's
draw
method can be called.
Currently, only one renderer is available:
BrowserSvgRenderer
: renders into an<svg>
element in a browser environment.
The following code shows how to parse some input, create the diagram and render it inside a browser environment:
import { compileToSequence, Diagram, BrowserSvgRenderer } from 'selena'
const input = '(the input source code would be here)'
const sequence = compileToSequence(input)
const diagram = Diagram.create(sequence)
// the following must run in the browser
// use horizontal and vertical padding: 50 extra pixels on each side
const svgRenderer = new BrowserSvgRenderer(50, 50)
diag.layout(svgRenderer)
svgRenderer.prepare(diag.getComputedSize())
diag.draw(svgRenderer)
const element = svgRenderer.finish()
// you can now append element to the DOM
Every Selena script consists of an Object Definition Section followed by a Message Section. There can be many object definitions and many messages. The language includes support for comments (pieces of text not to be included in the diagram).
Objects can be defined as follows:
object foo = "Foo"
object(actor) bar = "Bar"
This will create one component object with id foo
and label Foo
,
as well as one actor object with id bar
and label Bar
.
The object types differ in the way they are presented: Components are drawn as boxes, while actors are drawn as stick figures.
Messages (or activations) are written as arrows. Most types of messages can have other messages as children, i.e., functions calling other functions. For an object to send a message, it has to be active. An object is active for the duration it takes to handle a message. Initially, no object is active. A found message (message from the outside) can activate an object initially. Afterwards, more messages can follow. For example:
object foo = "Foo"
object bar = "Bar"
*->foo "a found message targeting foo" {
->bar "a message from foo to bar"
->foo "a message from foo to itself"
->bar {
->foo
}
}
*->bar
*->bar {}
The above script defines two objects foo
and bar
.
Then there is a found message activating foo
and a few nested messages on
multiple levels.
Note that labels are entirely optional, as are the braces ({
, }
) in case
there are no nested messages.
Message types
There are found messages from the outside to an object, regular messages between two objects, and lost messages from an object to the outside. Lost messages are specified as in the following example:
object foo = "Foo"
object bar = "Bar"
*->foo "foo is activated" {
->* "this message is leaving foo"
}
Again, the label is optional. Lost messages cannot have a block (as there is no execution context).
Regular messages have different types as well.
By default, they are synchronous.
By specifying async
, create
or destroy
the type of message can be
changed:
object foo = "Foo"
object bar = "Bar"
*->foo {
->(create) bar "this message creates bar"
->(async) bar "this is an asynchronous message" {
->foo "it can contain other messages"
->(async) foo "they can be of any type"
}
->(destroy) bar "this message destroys bar"
}
Asynchronous messages, like synchronous messages, can have children.
create
and destroy
messages cannot.
If you want to be explicit about a message's synchronicity, you can specify
its type as sync
, e.g., ->(sync) bar
, though this is not required.
Note that create
and destroy
will have a special effect on object
positioning inside the diagram and on the length of its lifeline.
Return values
By default, reply messages are automatically created for synchronous messages.
To specify the reply message label, use a return
statement:
object foo = "Foo"
object bar = "Bar"
*->foo {
->bar "message" {
return "reply"
}
}
The above will set the reply message label to read reply
.
Asynchronous messages do not include a reply by default. Mostly, it makes no sense to include one, either. Instead, an explicit second asynchronous message can serve as callback. To specify a reply regardless, the same syntax can be used:
object foo = "Foo"
object bar = "Bar"
*->foo {
->(async) bar "async message" {
return "with a reply"
}
}
No other type of message (lost, found, create
, destroy
) may have a
return
statement.
Comments can be used to include notes in the Selena script that should not
appear in the diagram.
They can be positioned anywhere (except inside of strings) and begin with the
hash symbol #
.
They extend to the end of the line.
For example:
# this is a comment
object foo = "Foo" # this is another comment