///| /// What a diagram renderer works with: the `` element to draw into /// and the configuration in effect. pub struct Context { /// The id of the `` element; ids inside the diagram derive from it. id : String config : @config.Config /// The root `` element. svg : @dom.Element /// The moment of rendering, in milliseconds since the epoch: what /// `new Date()` is to a diagram that shows the present. now : Double /// The page the diagram is rendered in. page : Page /// What loads images by URL, for the diagrams that need their size /// (see `@mermaid.render`). image_loader : ((String) -> Bytes?)? } ///| /// The page a diagram is rendered in, for the diagrams whose geometry /// depends on it. /// /// mermaid renders into a block element appended to the body, holding an /// ``. A Gantt chart takes the width of that element as /// its own, and measures its labels while the browser has the `` laid /// out at whatever size the page gives it — scrollbar included. pub struct Page { /// The viewport, in pixels. viewport_width : Double viewport_height : Double /// The margin of the body. margin : Double /// The width a vertical scrollbar takes from the page. scrollbar : Double /// The width of the screen that is free for windows /// (`screen.availWidth`), which a C4 diagram wraps its rows at. screen_width : Double } ///| /// A page; the defaults describe the one the reference renderings are /// made in: headless Chrome's default window on macOS, with the default /// body margin and classic scrollbars. pub fn Page::new( viewport_width~ : Double = 756.0, viewport_height~ : Double = 469.0, margin~ : Double = 8.0, scrollbar~ : Double = 15.0, screen_width~ : Double = 800.0, ) -> Page { { viewport_width, viewport_height, margin, scrollbar, screen_width, } } ///| pub fn Context::new( id : String, config : @config.Config, svg : @dom.Element, now~ : Double, page~ : Page, image_loader? : (String) -> Bytes?, ) -> Context { { id, config, svg, now, page, image_loader, } } ///| /// Blink's layout unit: lengths are kept in 64ths of a pixel, rounded /// down. fn floor_layout_unit(value : Double) -> Double { (value * 64.0).floor() / 64.0 } ///| /// The width of the element the diagram is rendered into, before the /// diagram has a size (`parentElement.offsetWidth` at the start of /// drawing). pub fn Context::container_width(self : Context) -> Double { self.page.viewport_width - 2.0 * self.page.margin } ///| /// The factor the browser draws the diagram's user units at once the /// `` (still `width="100%"`, no height) has the view box /// `0 0 width height`, the way the view box is fitted into the element /// (`preserveAspectRatio` at its default). /// /// The element is as wide as its container, or as `max_width` when its /// style limits it to less, and as tall as the view box's proportions /// make it; when that is taller than the viewport has room for, the page /// gets a scrollbar, the container narrows and the diagram shrinks with /// it. A diagram that measures text after setting its view box gets its /// measurements at this scale. /// /// With `fixed`, the element has `width` and `height` attributes of the /// size of the view box instead, and the page does not matter. pub fn Context::view_box_scale( self : Context, width : Double, height : Double, max_width? : Double, fixed~ : Bool = false, ) -> Double { let page = self.page // The width the element takes in a container. let fitted = (container : Double) => { match max_width { Some(limit) if floor_layout_unit(limit) < container => floor_layout_unit(limit) _ => container } } let (used_width, used_height) = if fixed { (floor_layout_unit(width), floor_layout_unit(height)) } else { let mut container = self.container_width() // Below the ``, an inline element, the line it stands on keeps // room for descenders in the font of the block mermaid renders into. let line_descent = match self.config.text("fontFamily") { Some(family) => @font.Stack::for_family(family).line_descent(16.0) // No font of its own: the default serif of the page. None => 4.0 } let used_height = floor_layout_unit(fitted(container) * height / width) // The page scrolls when its content, rounded to whole pixels, is // taller than the viewport; the scrollbar stays even if the narrower // content would fit again. if (used_height + line_descent + 2.0 * page.margin + 0.5).floor() > page.viewport_height { container -= page.scrollbar } let used_width = fitted(container) (used_width, floor_layout_unit(used_width * height / width)) } // SVG lengths are single precision. let width = Float::from_double(width).to_double() let height = Float::from_double(height).to_double() if width / height < used_width / used_height { used_height / height } else { used_width / width } } ///| /// The theme variables in effect. pub fn Context::theme(self : Context) -> @theme.Theme { self.config.theme() } ///| /// The font stack text in this diagram is measured with. pub fn Context::font( self : Context, bold~ : Bool = false, italic~ : Bool = false, ) -> @font.Stack { let family = self.theme().text("fontFamily") // A family emptied by the sanitizer (one with a hyphen in a theme // variable, such as `sans-serif`) leaves the stylesheet without a // `font-family`, and the text in the font of the page. Only its // regular face is embedded. if family.trim().is_empty() && !bold && !italic { return @font.Stack::page_default() } @font.Stack::for_family(family, bold~, italic~) } ///| /// Size the `` element, like mermaid's `configureSvgSize`: either /// responsive with a maximum width, or fixed. pub fn configure_svg_size( svg : @dom.Element, height : Double, width : Double, use_max_width : Bool, ) -> Unit { if use_max_width { svg .attr("width", "100%") .attr("style", "max-width: \{@dom.number(width)}px;") |> ignore } else { svg.attr_number("height", height).attr_number("width", width) |> ignore } } ///| /// mermaid's `parseFontSize`: the leading integer of a size such as /// `"17px"`, like JavaScript's `parseInt`. pub fn parse_font_size(size : String) -> Double? { let text = size.trim().to_owned() let mut value = 0.0 let mut digits = 0 let mut negative = false for i, c in text { if i == 0 && (c == '-' || c == '+') { negative = c == '-' continue } guard c is ('0'..<='9') else { break } value = value * 10.0 + (c.to_int() - '0').to_double() digits += 1 } if digits == 0 { None } else if negative { Some(-value) } else { Some(value) } } ///| /// `Number.prototype.toFixed(0)`: round to an integer, halves away from /// zero. pub fn to_fixed0(value : Double) -> String { let rounded = if value < 0.0 { -(-value + 0.5).floor() } else { (value + 0.5).floor() } @dom.number(rounded) }