///|
/// Why a diagram could not be rendered.
pub(all) suberror MermaidError {
/// No diagram type matches the text.
UnknownDiagram(String)
/// The diagram text is not valid for its type.
Syntax(String)
/// The frontmatter or configuration is invalid.
Config(String)
/// The diagram type is recognized but not ported yet.
Unsupported(String)
/// Upstream fails as well while drawing the diagram, with this message:
/// an image that cannot be decoded.
Render(String)
} derive(Eq, Debug)
///|
/// The failure as a message for people: what kind of failure it is, then
/// what went wrong.
pub impl Show for MermaidError with fn output(self, logger) {
let (kind, message) = match self {
UnknownDiagram(message) => ("unknown diagram", message)
Syntax(message) => ("syntax error", message)
Config(message) => ("invalid configuration", message)
Unsupported(message) => ("not supported", message)
Render(message) => ("cannot render", message)
}
logger.write_string(kind)
logger.write_string(": ")
logger.write_string(message)
}
///|
/// What a failure of the unified renderer is to the caller.
fn layout_failure(error : @rendering.LayoutError) -> MermaidError {
match error {
LayoutError(message) => Unsupported(message)
UpstreamError(message) => Render(message)
}
}
///|
/// Diagrams longer than this are refused unless `maxTextSize` says
/// otherwise.
const MAX_TEXT_LENGTH : Double = 50000.0
///|
/// What is drawn instead of a diagram longer than that.
const MAX_TEXT_LENGTH_EXCEEDED : String = "graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa"
///|
/// What a diagram contributes to the rendering pipeline once parsed.
priv struct Parsed {
styles : String
acc_title : String
acc_description : String
/// Classes defined in the diagram text, which become stylesheet rules.
classes : Array[StyleClass]
draw : (@diagram.Context) -> Unit raise MermaidError
}
///|
/// `id` is the id of the `` element, for the stylesheets that address
/// definitions inside it.
fn parse_diagram(
diagram_type : String,
code : String,
config : @config.Config,
title : String?,
id : String,
) -> Parsed raise MermaidError {
match diagram_type {
"pie" => {
let pie = @pie.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
}
// A frontmatter title applies unless the diagram body has its own.
let pie = match title {
Some(title) if pie.title == "" => pie.with_title(title)
_ => pie
}
{
styles: @pie.styles(config.theme()),
acc_title: pie.acc_title,
acc_description: pie.acc_description,
classes: [],
draw: context => {
@pie.draw(pie, context) catch {
@pie.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"info" => {
@info.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
}
{
styles: "",
acc_title: "",
acc_description: "",
classes: [],
draw: context => @info.draw(context),
}
}
"packet" => {
let packet = @packet.parse(code, config) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
}
let packet = match title {
Some(title) if packet.title == "" => packet.with_title(title)
_ => packet
}
{
styles: @packet.styles(config.theme()),
acc_title: packet.acc_title,
acc_description: packet.acc_description,
classes: [],
draw: context => @packet.draw(packet, context),
}
}
"flowchart-v2" | "flowchart-elk" => {
let db = @flowchart.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@flowchart.FlowError(message) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) && db.title == "" {
db.set_title(title)
}
{
styles: @flowchart.styles(
config.theme(),
config.text("theme").unwrap_or(""),
config.text("look").unwrap_or(""),
),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: db.classes
.values()
.map(c => { id: c.id, styles: c.styles, text_styles: c.text_styles, })
.collect(),
draw: context => {
@flowchart.draw(db, context, diagram_type~) catch {
error => raise layout_failure(error)
}
},
}
}
"agentflow" => {
let db = @agentflow.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@agentflow.AgentflowError(message) => raise Syntax(message)
@flowchart.FlowError(message) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) && db.title == "" {
db.set_title(title)
}
{
styles: @agentflow.styles(
config.theme(),
config.text("theme").unwrap_or(""),
config.text("look").unwrap_or(""),
),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: db.flow.classes
.values()
.map(c => { id: c.id, styles: c.styles, text_styles: c.text_styles, })
.collect(),
draw: context => {
@agentflow.draw(db, context) catch {
error => raise layout_failure(error)
}
},
}
}
"swimlane" => {
// A flowchart laid out in lanes: the flowchart's parser, database and
// renderer, with the lane rules added to its stylesheet.
let db = @flowchart.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@flowchart.FlowError(message) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) && db.title == "" {
db.set_title(title)
}
{
styles: @swimlanes.styles(
config.theme(),
config.text("theme").unwrap_or(""),
config.text("look").unwrap_or(""),
),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: db.classes
.values()
.map(c => { id: c.id, styles: c.styles, text_styles: c.text_styles, })
.collect(),
draw: context => {
@flowchart.draw(db, context, diagram_type="swimlane") catch {
error => raise layout_failure(error)
}
},
}
}
"gitGraph" => {
let graph = @gitgraph.parse(code, config) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@gitgraph.GitError(message) => raise Syntax(message)
@gitgraph.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) && graph.title == "" {
graph.set_title(title) catch {
@gitgraph.Unsupported(message) => raise Unsupported(message)
}
}
{
styles: @gitgraph.styles(config, "#" + id),
acc_title: graph.acc_title,
acc_description: graph.acc_description,
classes: [],
draw: context => {
@gitgraph.draw(graph, context) catch {
@gitgraph.GitError(message) => raise Syntax(message)
@gitgraph.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
},
}
}
"c4" => {
let convert = (error : Error) => {
match error {
@jison.ParseError(message~, ..) => Syntax(message)
@c4.C4Error(message) => Syntax(message)
@c4.Unsupported(message) => Unsupported(message)
error => Syntax(error.to_string())
}
}
let db = @c4.parse(code, config) catch { error => raise convert(error) }
// A frontmatter title is not shown: the database has no
// `setDiagramTitle`.
{
styles: @c4.styles(config) catch {
error => raise convert(error)
},
acc_title: "",
acc_description: db.acc_description,
classes: [],
draw: context => {
@c4.draw(db, context) catch {
error => raise convert(error)
}
},
}
}
"journey" => {
let journey = @journey.parse(code) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@journeycommon.SanitizeError(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) && journey.title == "" {
journey.set_title(title)
}
{
styles: @journey.styles(config.theme()),
acc_title: journey.acc_title,
acc_description: journey.acc_description,
classes: [],
draw: context => {
@journey.draw(journey, context) catch {
@journey.JourneyError(message) => raise Unsupported(message)
@journeycommon.MeasureError(message) => raise Unsupported(message)
error => raise Unsupported(error.to_string())
}
},
}
}
"mindmap" => {
let db = @mindmap.parse(code, config) catch {
@mindmap.Invalid(message) => raise Syntax(message)
@mindmap.Unsupported(message) => raise Unsupported(message)
}
{
styles: @mindmap.styles(config, "#" + id),
acc_title: "",
acc_description: "",
classes: [],
draw: context => {
@mindmap.draw(db, context) catch {
@mindmap.Invalid(message) => raise Syntax(message)
@mindmap.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"er" => {
let db = @er.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) {
db.set_title(title)
}
{
styles: @er.styles(config),
acc_title: db.acc_title,
acc_description: db.acc_description,
// The renderer reports no classes, so none become stylesheet rules.
classes: [],
draw: context => {
@er.draw(db, context) catch {
error => raise layout_failure(error)
}
},
}
}
"requirement" => {
let db = @requirement.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) {
db.set_title(title)
}
{
styles: @requirement.styles(config),
acc_title: db.acc_title,
acc_description: db.acc_description,
// The renderer reports no classes, so none become stylesheet rules.
classes: [],
draw: context => {
@requirement.draw(db, context) catch {
error => raise layout_failure(error)
}
},
}
}
"stateDiagram" => {
let db = @state.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@state.StateError(message) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) {
db.set_title(title) catch {
@state.StateError(message) => raise Syntax(message)
}
}
{
styles: @state.styles(
config.theme(),
config.text("theme").unwrap_or(""),
config.text("look").unwrap_or(""),
"#" + id,
),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: db.classes
.values()
.map(c => { id: c.id, styles: c.styles, text_styles: c.text_styles, })
.collect(),
draw: context => {
@state.draw(db, context) catch {
error => raise layout_failure(error)
}
},
}
}
"timeline" => {
let timeline = @timeline.parse(code) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@journeycommon.SanitizeError(message) => raise Unsupported(message)
@timeline.TimelineError(message) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
// The timeline's database exposes neither `setDiagramTitle` nor the
// accessibility getters to the renderer, so a frontmatter title and
// `accTitle`/`accDescr` leave no trace in the output.
{
styles: @timeline.styles(
config.theme(),
config.text("theme").unwrap_or(""),
id,
),
acc_title: "",
acc_description: "",
classes: [],
draw: context => {
@timeline.draw(timeline, context) catch {
@timeline.TimelineError(message) => raise Unsupported(message)
@journeycommon.MeasureError(message) => raise Unsupported(message)
error => raise Unsupported(error.to_string())
}
},
}
}
"gantt" => {
let db = @gantt.parse(code, title?) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@gantt.GanttError(message) => raise Syntax(message)
@gantt.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
{
styles: @gantt.styles(config.theme()),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: [],
draw: context => {
// Dates are only worked out when the chart is drawn, and may
// prove invalid then.
@gantt.draw(db, context) catch {
@gantt.GanttError(message) => raise Syntax(message)
@gantt.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"sequence" => {
let db = @sequence.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@sequence.SequenceError(message) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) && db.title == "" {
db.set_title(title)
}
{
styles: @sequence.styles(
config.theme(),
config.text("look").unwrap_or(""),
),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: [],
draw: context => {
@sequence.draw(db, context) catch {
@sequence.SequenceError(message) => raise Unsupported(message)
}
},
}
}
"sankey" => {
let sankey = @sankey.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@sankey.Invalid(message) => raise Syntax(message)
@sankey.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
{
styles: @sankey.styles(config.theme()),
acc_title: "",
acc_description: "",
classes: [],
draw: context => {
@sankey.draw(sankey, context) catch {
@sankey.Invalid(message) => raise Syntax(message)
@sankey.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"architecture" => {
let db = @architecture.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@architecture.Invalid(message) => raise Syntax(message)
@architecture.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
// The diagram title is kept but not drawn.
ignore(title)
{
styles: @architecture.styles(config.theme()),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: [],
draw: context => {
@architecture.draw(db, context) catch {
@architecture.Invalid(message) => raise Syntax(message)
@architecture.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"radar" => {
let radar = @radar.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@radar.Invalid(message) => raise Syntax(message)
@radar.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
let radar = match title {
Some(title) if radar.title == "" => radar.with_title(title)
_ => radar
}
{
styles: @radar.styles(config.theme()),
acc_title: radar.acc_title,
acc_description: radar.acc_description,
classes: [],
draw: context => @radar.draw(radar, context),
}
}
"treemap" => {
let treemap = @treemap.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@treemap.Invalid(message) => raise Syntax(message)
@treemap.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) && treemap.title == "" {
treemap.set_title(title)
}
{
styles: @treemap.styles(config.theme()),
acc_title: treemap.acc_title,
acc_description: treemap.acc_description,
classes: treemap.classes
.values()
.map(c => { id: c.id, styles: c.styles, text_styles: c.text_styles, })
.collect(),
draw: context => {
@treemap.draw(treemap, context) catch {
@treemap.Invalid(message) => raise Syntax(message)
@treemap.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"block" => {
let db = @block.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@block.BlockError(message) => raise Syntax(message)
error => raise Syntax(error.to_string())
}
{
styles: @block.styles(
config.theme(),
config.text("theme").unwrap_or(""),
config.text("look").unwrap_or(""),
),
// The block database keeps no accessibility text.
acc_title: "",
acc_description: "",
classes: db.classes
.values()
.map(c => { id: c.id, styles: c.styles, text_styles: c.text_styles, })
.collect(),
draw: context => {
@block.draw(db, context) catch {
@rendering.LayoutError(message) => raise Unsupported(message)
@rendering.UpstreamError(message) => raise Render(message)
@block.BlockError(message) => raise Syntax(message)
error => raise Unsupported(error.to_string())
}
},
}
}
"classDiagram" => {
let db = @classdiagram.parse(code, config) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@classdiagram.Invalid(message) => raise Syntax(message)
@classdiagram.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
if title is Some(title) {
db.set_title(title)
}
// Upstream hands the classes themselves to the code that writes
// rules for `classDef`s: a class with styles gets rules under its
// own name.
let classes = db.styled_classes() catch {
@classdiagram.Invalid(message) => raise Syntax(message)
@classdiagram.Unsupported(message) => raise Unsupported(message)
}
{
styles: @classdiagram.styles(config),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: classes.map(c => { id: c.0, styles: c.1, text_styles: [], }),
draw: context => {
@classdiagram.draw(db, context) catch {
error => raise layout_failure(error)
}
},
}
}
"usecase" => {
let db = @usecase.parse(code, config) catch {
@usecase.Invalid(message) => raise Syntax(message)
@usecase.Unsupported(message) => raise Unsupported(message)
}
// A frontmatter title is not shown: parsing clears the title the
// database was given before.
{
styles: @usecase.styles(config),
acc_title: db.acc_title,
acc_description: db.acc_description,
classes: [],
draw: context => {
@usecase.draw(db, context) catch {
@usecase.Invalid(message) => raise Syntax(message)
@usecase.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"quadrantChart" => {
let chart = @quadrant.parse(code, title?) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@quadrant.QuadrantError(message) => raise Syntax(message)
@journeycommon.SanitizeError(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
{
// The chart has no styles of its own.
styles: "",
acc_title: chart.acc_title,
acc_description: chart.acc_description,
classes: [],
draw: context => {
@quadrant.draw(chart, context) catch {
@quadrant.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"xychart" => {
let chart = @xychart.parse(code, title?) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@journeycommon.SanitizeError(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
{
// The chart has no styles of its own.
styles: "",
acc_title: chart.acc_title,
acc_description: chart.acc_description,
classes: [],
draw: context => {
@xychart.draw(chart, context) catch {
@xychart.XYChartError(message) => raise Syntax(message)
@xychart.Unsupported(message) => raise Unsupported(message)
error => raise Unsupported(error.to_string())
}
},
}
}
"treeView" => {
let tree = @treeview.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@treeview.TreeViewError(message) => raise Syntax(message)
@treeview.Unsupported(message) => raise Unsupported(message)
@journeycommon.SanitizeError(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
{
styles: @treeview.styles(config.theme()),
acc_title: tree.acc_title,
acc_description: tree.acc_description,
classes: [],
draw: context => {
@treeview.draw(tree, context) catch {
@treeview.TreeViewError(message) => raise Syntax(message)
@treeview.Unsupported(message) => raise Unsupported(message)
error => raise Unsupported(error.to_string())
}
},
}
}
"ishikawa" => {
let diagram = @ishikawa.parse(code) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@ishikawa.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
// The grammar has no accessibility statements, and the title — the
// effect — is drawn as the head, not as a title.
{
styles: @ishikawa.styles(config.theme()),
acc_title: "",
acc_description: "",
classes: [],
draw: context => {
@ishikawa.draw(diagram, context) catch {
@ishikawa.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"cynefin" => {
let diagram = @cynefin.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@cynefin.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
// A frontmatter title applies unless the diagram body has its own.
if title is Some(title) && diagram.title == "" {
diagram.set_title(title) catch {
@cynefin.Unsupported(message) => raise Unsupported(message)
}
}
{
styles: @cynefin.styles(config.theme()),
acc_title: diagram.acc_title,
acc_description: diagram.acc_description,
classes: [],
draw: context => {
@cynefin.draw(diagram, context) catch {
@cynefin.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"railroad" | "railroadEbnf" | "railroadAbnf" | "railroadPeg" => {
let railroad = @railroad.parse(diagram_type, code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@railroad.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
// Nothing draws a title, the frontmatter's no more than the
// diagram's own.
{
styles: @railroad.styles(config),
acc_title: railroad.acc_title,
acc_description: railroad.acc_description,
classes: [],
draw: context => {
@railroad.draw(railroad, context) catch {
@railroad.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"eventmodeling" => {
let model = @eventmodeling.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@eventmodeling.Invalid(message) => raise Syntax(message)
@eventmodeling.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
// The diagram has no styles of its own, and nothing draws a title.
{
styles: "",
acc_title: model.acc_title,
acc_description: model.acc_description,
classes: [],
draw: context => {
@eventmodeling.draw(model, context) catch {
@eventmodeling.Invalid(message) => raise Syntax(message)
@eventmodeling.Unsupported(message) => raise Unsupported(message)
error => raise Unsupported(error.to_string())
}
},
}
}
"wardley" => {
let map = @wardley.parse(code) catch {
@langium.ParseError(message~, ..) => raise Syntax(message)
@wardley.Invalid(message) => raise Syntax(message)
@wardley.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
// A frontmatter title applies unless the diagram body has its own.
if title is Some(title) && map.title == "" {
map.set_title(title)
}
{
// The renderer empties the `` before drawing, stylesheet
// included, so the diagram's styles never reach the output.
styles: "",
acc_title: map.acc_title,
acc_description: map.acc_description,
classes: [],
draw: context => {
@wardley.draw(map, context) catch {
@wardley.Invalid(message) => raise Syntax(message)
@wardley.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"venn" => {
let venn = @venn.parse(code) catch {
@jison.ParseError(message~, ..) => raise Syntax(message)
@venn.Invalid(message) => raise Syntax(message)
@venn.Unsupported(message) => raise Unsupported(message)
error => raise Syntax(error.to_string())
}
// A frontmatter title applies unless the diagram body has its own.
if title is Some(title) && venn.title == "" {
venn.set_title(title)
}
{
styles: @venn.styles(config.theme()),
acc_title: venn.acc_title,
acc_description: venn.acc_description,
classes: [],
draw: context => {
@venn.draw(venn, context) catch {
@venn.Invalid(message) => raise Syntax(message)
@venn.Unsupported(message) => raise Unsupported(message)
}
},
}
}
"kanban" => {
let convert = (error : Error) => {
match error {
@jison.ParseError(message~, ..) => Syntax(message)
@kanban.Invalid(message) => Syntax(message)
@kanban.Unsupported(message) => Unsupported(message)
@rendering.LayoutError(message) => Unsupported(message)
@rendering.UpstreamError(message) => Render(message)
error => Syntax(error.to_string())
}
}
let db = @kanban.parse(code, config) catch {
error => raise convert(error)
}
// The database keeps neither a title nor accessibility text.
{
styles: @kanban.styles(config.theme()) catch {
error => raise convert(error)
},
acc_title: "",
acc_description: "",
classes: [],
draw: context => {
@kanban.draw(db, context) catch {
error => raise convert(error)
}
},
}
}
other => raise Unsupported(other)
}
}
///|
/// Render diagram text to SVG.
///
/// - `id` becomes the id of the `` element and the prefix of the ids
/// inside it, as with `mermaid.render(id, text)`.
/// - `config` is the site configuration, as passed to
/// `mermaid.initialize`. The diagram's own frontmatter and directives are
/// layered on top.
///
/// - `now` is the moment of rendering in milliseconds since the epoch, for
/// the diagrams that show the present (the today marker of a Gantt
/// chart). It defaults to the system clock, as in mermaid.
/// - `page` describes the page the diagram is rendered in, which a Gantt
/// chart takes its width from. The default is the page the reference
/// renderings are made in.
/// - `image_loader` loads the images of image nodes (`@{ img: ... }`),
/// whose natural size mermaid takes from the image: it gives the bytes
/// of the image at a URL, or `None` when it cannot load it. Rendering
/// does no input or output of its own: without a loader, or when it
/// gives `None`, an image is drawn only when the node gives its width
/// and height without a constraint, and the rendering fails with
/// `Unsupported` otherwise. Images in `data:` URLs need no loader. An
/// image that is no image fails as upstream does, with `Render`.
///
/// The output is byte for byte what mermaid produces in the reference
/// browser, for the diagram types and features ported so far. Local times
/// are those of the reference machine's time zone, Asia/Shanghai.
pub fn render(
text : String,
id~ : String = "mermaid-svg",
config~ : Json = Json::object(Map([])),
now? : Double,
page~ : @diagram.Page = @diagram.Page::new(),
image_loader? : (String) -> Bytes?,
) -> String raise MermaidError {
let processed = @preprocess.preprocess(text) catch {
@preprocess.UnknownDiagramError(message) => raise UnknownDiagram(message)
error => raise Config(error.to_string())
}
let diagram_type = @preprocess.detect_type(processed.code) catch {
@preprocess.UnknownDiagramError(message) => raise UnknownDiagram(message)
}
let config = @config.Config::new(
site=config,
directive=processed.config,
diagram_type~,
) catch {
@config.InvalidColor(color) =>
raise Config("Unsupported color format: \"\{color}\"")
}
// Text longer than `maxTextSize` is replaced by a flowchart saying so,
// drawn with the configuration of the diagram it replaces.
let max_length = config.number("maxTextSize").unwrap_or(MAX_TEXT_LENGTH)
let (code, with_comments, diagram_type) = if processed.code
.length()
.to_double() >
max_length {
let code = MAX_TEXT_LENGTH_EXCEEDED
let diagram_type = @preprocess.detect_type(code) catch {
@preprocess.UnknownDiagramError(message) => raise UnknownDiagram(message)
}
(code, code, diagram_type)
} else {
(processed.code, processed.with_comments, diagram_type)
}
let svg = @dom.Element::new("svg")
.attr("id", id)
.attr("width", "100%")
.attr("xmlns", "http://www.w3.org/2000/svg")
let first_child = svg.append("g")
// The event modeling and agentflow databases ask for the text with its
// comments (`preserveCommentsWhenParsing`), as long as that is not too
// long.
let source = if diagram_type is ("eventmodeling" | "agentflow") &&
with_comments.length().to_double() <= max_length {
with_comments
} else {
code
}
// Entity codes are shielded from the grammars, as in Diagram.fromText.
let parsed = parse_diagram(
diagram_type,
@preprocess.encode_entities(source) + "\n",
config,
processed.title,
id,
)
// Upstream's render hands back an