term-transcript 0.5.0

Snapshotting and snapshot testing for CLI / REPL applications
Documentation
{{!
  Computes content height based on line count in interactions.
  Expected hash inputs: `interactions`, `const`, `line_height`.
}}
{{~#*inline "compute_content_height"}}
  {{#scope lines=0 margins=0 displayed_interactions=0}}
    {{#each interactions}}
      {{#if (not input.hidden)}}
        {{set @../lines (add @../lines (count_lines input.text))}}
        {{set @../margins (add @../margins 1)}}
        {{set @../displayed_interactions (add @../displayed_interactions 1)}}
      {{/if}}
      {{set @../lines (add @../lines (len output))}}
      {{#if (ne 0 (len output))}}
        {{set @../margins (add @../margins 1)}}
      {{/if}}
    {{/each}}
    {{#if (gt @margins 0)}}
      {{! The last margin is not displayed. }}
      {{set @margins (sub @margins 1)}}
    {{/if}}
    {{#debug lines=@lines margins=@margins displayed_interactions=@displayed_interactions~}}
      computed params for content height
    {{~/debug}}
    {{add (mul @lines line_height)
          (mul @margins const.BLOCK_MARGIN)
          (mul @displayed_interactions (mul 2 const.USER_INPUT_PADDING)) }}
  {{/scope}}
{{/inline~}}

{{! Common styles header, e.g. `@font-face`s }}
{{~#*inline "common_styles_header"}}
{{~#if additional_styles}}

{{{additional_styles}}}
{{~/if~}}
{{~#with embedded_font~}}
{{~#each faces}}
@font-face {
  font-family: "{{../family_name}}";
  src: url("data:{{mime_type}};base64,{{{base64_data}}}");
  {{#if (ne is_bold null)}}
  font-weight: {{#if is_bold}}bold{{else}}normal{{/if}};
  {{/if}}
  {{#if (ne is_italic null)}}
  font-style: {{#if is_italic}}italic{{else}}normal{{/if}};
  {{/if}}
}
{{/each~}}
{{~else~}}
{{~/with~}}
{{/inline~}}

{{! Common font styling }}
{{~#*inline "common_font_styles"}}
.bold,.prompt { font-weight: bold;{{#with embedded_font.metrics.bold_spacing as |sp|}} letter-spacing: {{round sp digits=4}}em;{{else}}{{/with}} }
.italic { font-style: italic;{{#with embedded_font.metrics.italic_spacing as |sp|}} letter-spacing: {{round sp digits=4}}em;{{else}}{{/with}} }
{{/inline~}}

{{! Terminal background }}
{{~#*inline "background"}}
<rect width="100%" height="100%" y="{{#if window}}-{{const.WINDOW_FRAME_HEIGHT}}{{else}}0{{/if}}" rx="4.5" style="fill: {{ palette.colors.black }};"/>
{{~#if window}}

<rect width="100%" height="26" y="-22" clip-path="inset(0 0 -10 0 round 4.5)" style="fill: #fff; fill-opacity: 0.1;"/>
<circle cx="17" cy="-9" r="7" style="fill: {{ palette.colors.red }};"/>
<circle cx="37" cy="-9" r="7" style="fill: {{ palette.colors.yellow }};"/>
<circle cx="57" cy="-9" r="7" style="fill: {{ palette.colors.green }};"/>
{{#if window.title~}}
<text x="50%" y="-3.5" style="font: bold 14px system-ui, sans-serif; text-anchor: middle; fill: {{ palette.colors.white }};">{{window.title}}</text>
{{~/if~}}
{{~/if}}

{{/inline~}}

{{!
  Computes scroll animation parameters.
  Expected hash inputs: `content_height`, `const`, `scroll`, `width`
}}
{{~#*inline "compute_scroll_animation"}}
  {{#if (gte scroll.max_height content_height)}}
  {{! No need for scroll animation }}
    null
  {{else}}
    {{#scope
      steps=(round (div (sub content_height scroll.max_height) scroll.pixels_per_scroll) mode="up")
      scroll_height=(sub content_height scroll.max_height)
      scrollbar_height=(max
        (min scroll.min_scrollbar_height scroll.max_height)
        (round (mul scroll.max_height (div scroll.max_height content_height)))
      )
      pos=0
      view_box=""
      scrollbar_y=""
      sep=""
    }}
      {{#debug steps=@steps scrollbar_height=@scrollbar_height~}}computed scroll animation params{{/debug}}
      {{#each (range 0 (add @steps 1))}}
        {{set @../pos (min @../scroll_height (mul ../scroll.pixels_per_scroll @index))}}
        {{! Skip the penultimate step if its position is too close to the last one }}
        {{#if (or @last (gt (sub @../scroll_height @../pos) (mul ../scroll.pixels_per_scroll ../scroll.elision_threshold)))}}
          {{#set @../sep}}{{#if @first}}""{{else}}";"{{/if}}{{/set}}
          {{#set @../view_box append=true}}{{@../sep}}0 {{round @../pos digits=1}} {{../width}} {{../scroll.max_height}}{{/set}}
          {{#set @../scrollbar_y append=true}}{{@../sep}}{{round
            (add
              (mul (div @../pos @../scroll_height) (sub ../scroll.max_height @../scrollbar_height))
              ../const.WINDOW_PADDING
            )
            digits=1
          }}{{/set}}
        {{else}}
          {{#debug}}eliding last scroll keyframe{{/debug}}
          {{set @../steps (sub @../steps 1)}}
        {{/if}}
      {{/each}}

      {
        "duration": {{mul scroll.interval @steps}},
        "view_box": "{{@view_box}}",
        "scrollbar_height": {{@scrollbar_height}},
        "scrollbar_x": {{sub width (add const.SCROLLBAR_RIGHT_OFFSET 5)}}, {{! 5 is scrollbar width }}
        "scrollbar_y": "{{@scrollbar_y}}"
      }
    {{/scope}}
  {{/if}}
{{/inline~}}

{{! Scrollbar }}
{{~#*inline "scrollbar"}}
  {{#with scroll_animation}}
<rect id="scrollbar" class="scrollbar" x="{{scrollbar_x}}" y="10" width="5" height="{{scrollbar_height}}" />
  {{/with}}
{{/inline~}}

{{! Scrolling animations }}
{{~#*inline "scroll_animation"}}
  {{#with scroll_animation}}
<animate id="scroll" href="#scroll-container" attributeName="viewBox" values="{{view_box}}" begin="0s" dur="{{duration}}s" repeatCount="indefinite" calcMode="discrete" />
<animate href="#scrollbar" attributeName="y" values="{{scrollbar_y}}" begin="scroll.begin" dur="{{duration}}s" repeatCount="indefinite" calcMode="discrete" />
  {{/with}}
{{/inline~}}

{{! Renders an HTML span }}
{{~#*inline "html_span"}}
  {{~#if (gt (len this) 1)~}}{{! Are there any style properties in `span`? }}
    {{~#scope fg=fg bg=bg classes="" styles=""~}}
    {{~#if inverted~}}
      {{~set @fg bg~}}
      {{~set @bg fg~}}
    {{~/if~}}
    {{~#set @classes append=true}}
      {{~>common_span_classes this fg=@fg bg=@bg~}}
      {{~#if (and dimmed (ne (typeof @bg) "null"))}} dimmed-bg{{/if~}}
      {{~#if (eq (typeof @fg) "number")}} fg{{@fg}}{{/if~}}
      {{~#if (eq (typeof @bg) "number")}} bg{{@bg}}{{/if~}}
    {{/set~}}
    {{~set @classes (trim @classes)~}}
    {{~#set @styles append=true}}
      {{~#if (eq (typeof @fg) "string")}} color: {{@fg}};{{/if~}}
      {{~#if (eq (typeof @bg) "string")}} background: {{@bg}};{{/if~}}
    {{/set~}}
    {{~set @styles (trim @styles)~}}
    <span{{#if @classes}} class="{{@classes}}"{{/if}}{{#if @styles}} style="{{@styles}}"{{/if}}>
    {{~/scope~}}
  {{~/if~}}
  {{!
    If dimmed or blinking, enclose text in an additional span to apply pseudo-opacity effects based on `currentColor` and/or
    by having a semi-transparent background; see .dimmed / .blink CSS. Using `opacity` or filters doesn't work in Safari.
  }}
  {{#if (or dimmed blink)}}<span>{{/if}}{{text}}{{#if (or dimmed blink)}}</span>{{/if}}
  {{~#if (gt (len this) 1)~}}
    </span>
  {{~/if~}}
{{~/inline~}}

{{! Swaps background and foreground colors. If any are not specified, use the default palette colors. }}
{{~#*inline "common_span_classes"}}
  {{~#if bold}} bold{{/if~}}
  {{~#if italic}} italic{{/if~}}
  {{~#if underline}} underline{{/if~}}
  {{~#if strikethrough}} strike{{/if~}}
  {{~#if dimmed}} dimmed{{/if~}}
  {{~#if blink}} blink{{/if~}}
  {{~#if concealed}} concealed{{/if~}}
  {{~#if inverted}} inv{{/if~}}
  {{~#if (and inverted (eq (typeof fg) "null"))}} fg-none{{/if~}}
  {{~#if (and inverted (eq (typeof bg) "null"))}} bg-none{{/if~}}
{{~/inline~}}

{{! Renders an SVG foreground <tspan> styling attributes (class and/or style) }}
{{~#*inline "svg_tspan_attrs"}}
  {{~#scope fg=fg bg=bg classes="" styles=""}}
  {{~#if (gt (len this) 1)~}}{{! Are there any style properties in `span`? }}
    {{~#if inverted~}}
      {{~set @fg bg~}}
      {{~set @bg fg~}}
    {{~/if~}}
    {{~#set @classes append=true}}
      {{~>common_span_classes this fg=@fg bg=@bg~}}
      {{~#if (eq (typeof @fg) "number")}} fg{{@fg}}{{/if~}}
      {{! Unlike with HTML <span>s, we add the bg class in any case to assist with parsing }}
      {{~#if (ne (typeof @bg) "null")}} bg{{@bg}}{{/if~}}
    {{/set~}}
    {{~set @classes (trim @classes)~}}
    {{~#set @styles append=true}}
      {{~#if (eq (typeof @fg) "string")}}fill: {{@fg}};{{/if~}}
    {{/set~}}
  {{~/if~}}
  {{#if @classes}} class="{{@classes}}"{{/if}}{{#if @styles}} style="{{@styles}}"{{/if}}
  {{~/scope~}}
{{~/inline~}}

{{!
  Scales font metrics from design units to ems to pixels.
  Expected hash inputs: `metrics`, `font_size`
}}
{{~#*inline "scale_font_metrics"}}
  {
    "advance_width": {{mul font_size (div metrics.advance_width metrics.units_per_em)}},
    "ascent": {{mul font_size (div metrics.ascent metrics.units_per_em)}},
    "descent": {{mul font_size (div metrics.descent metrics.units_per_em)}}
  }
{{/inline~}}

{{! Sets `line_height` based on font metrics (if supplied) or the user-provided value }}
{{~#*inline "compute_line_height"}}
  {{~#if (eq line_height null) ~}}
    {{#if embedded_font}}
      {{~#scope metrics=null ~}}
        {{~#set @metrics~}}
          {{~>scale_font_metrics metrics=embedded_font.metrics font_size=const.FONT_SIZE ~}}
        {{~/set~}}
        {{~#debug metrics=@metrics}}using line height from font metrics{{/debug~}}
        {{~round (sub (lookup @metrics "ascent") (lookup @metrics "descent")) digits=1~}}
      {{~/scope~}}
    {{~else~}}
      {{~#debug}}using default line height{{/debug~}}
      16.8 {{! Set to a somewhat reasonable default (1.2 * font_size) }}
    {{~/if~}}
  {{~else~}}
    {{~#debug line_height=line_height}}using user-provided line height{{/debug~}}
    {{! Scale line height according to the font size }}
    {{~round (mul line_height const.FONT_SIZE) digits=1~}}
  {{~/if~}}
{{/inline~}}

{{!
  Include main rendering logic. We need this roundabout construction in order for common inlines to be available
  to the format-specific rendering logic.
}}
{{~>main~}}