term-transcript 0.5.0

Snapshotting and snapshot testing for CLI / REPL applications
Documentation
{{! Root template }}
{{~#*inline "root"}}
<!-- Created with {{{creator.name}}} v{{{creator.version}}} ({{{creator.repo}}}) -->
<svg viewBox="0 {{#if window}}-{{const.WINDOW_FRAME_HEIGHT}}{{else}}0{{/if}} {{width}} {{height}}" width="{{width}}" height="{{height}}" xmlns="http://www.w3.org/2000/svg">
  <switch>
    <g requiredExtensions="http://www.w3.org/1999/xhtml">
      {{>styles}}
      {{>background}}

      {{~>content}}
      {{~#if scroll_animation}}
      {{>scrollbar}}
      {{>scroll_animation}}
      {{/if}}
    </g>
    {{>unsupported_error}}
  </switch>
</svg>
{{/inline~}}

{{! NB. The warning text should fit in one 80-char line to not potentially overflow the viewbox. }}
{{~#*inline "unsupported_error"}}
<text x="10" y="{{line_height}}" style="font: {{const.FONT_SIZE}}px monospace; fill: {{ palette.colors.red }};">
  HTML embedding not supported.
  Consult <tspan style="text-decoration: underline; text-decoration-thickness: 1px;"><a href="https://github.com/slowli/term-transcript/blob/HEAD/FAQ.md">term-transcript docs</a></tspan> for details.
</text>
{{/inline~}}

{{! CSS definitions }}
{{~#*inline "styles"}}
<style>
  {{>common_styles_header}}
  .container {
    padding: 0 {{const.WINDOW_PADDING}}px;
    color: {{ palette.colors.white }};
    line-height: {{line_height}}px;
  }
  .container pre {
    padding: 0;
    margin: 0;
    font: {{const.FONT_SIZE}}px {{#if embedded_font}}"{{embedded_font.family_name}}", monospace{{else}}{{font_family}}{{/if}};
    line-height: inherit;
  }
  .input {
    {{#if line_numbers~}}
    {{~#if (eq line_numbers.scope "continuous")}}
    display: flex;
    {{/if~}}
    {{~/if}}
    margin: 0 -{{const.WINDOW_PADDING}}px {{const.BLOCK_MARGIN}}px;
    color: {{ palette.colors.white }};
    background: rgba(255, 255, 255, 0.1);
    padding: 2px {{const.WINDOW_PADDING}}px;
  }
  .input-hidden { display: none; }
  {{#if line_numbers}}{{#if (eq line_numbers.scope "continuous")~}}
  .input > pre { flex-grow: 1; }
  {{/if}}{{/if~}}
  .output { {{#if line_numbers}}display: flex; {{/if}}margin-bottom: {{const.BLOCK_MARGIN}}px; }
  {{#if line_numbers}}
  .output > pre { flex-grow: 1; }
  pre.line-numbers {
    flex-grow: 0;
    width: {{const.LN_WIDTH}}px;
    text-align: right;
    padding-right: {{const.LN_PADDING}}px;
    color: color-mix(in hsl, currentColor 35%, {{ palette.colors.black }});
    user-select: none;
  }
  {{/if}}
  .output-sep {
    margin: -{{div const.BLOCK_MARGIN 2}}px -{{const.WINDOW_PADDING}}px {{sub (div const.BLOCK_MARGIN 2) 1}}px;
    border-top: 1px solid color-mix(in hsl, {{ palette.colors.white }} 35%, {{ palette.colors.black }});
  }
  {{#if has_failures~}}
  .input-failure {
    border-left: 2px solid {{ palette.colors.red }};
    border-right: 2px solid {{ palette.colors.red }};
    background: rgba(255, 0, 65, 0.15);
  }
  {{/if}}
  {{~#if scroll_animation}}
  .scrollbar { fill: #fff; fill-opacity: 0.35; }
  {{/if}}
  {{>common_font_styles}}
  {{!
    For opacity-related styling (.dimmed, .blink), text decoration must be applied to the child <span>,
    so that it has the correct opacity.
  }}
  .underline:not(.dimmed, .blink), .underline:is(.dimmed, .blink) > span { text-decoration-line: underline; }
  .strike:not(.dimmed, .blink), .strike:is(.dimmed, .blink) > span { text-decoration-line: line-through; }
  .underline.strike:not(.dimmed, .blink), .underline.strike:is(.dimmed, .blink) > span { text-decoration-line: underline line-through; }
  {{!
    The dimmed-related complexity is motivated by Safari quirks; it doesn't render HTML-in-SVG with any filters
    (incl. opacity) correctly.
  }}
  {{#scope opacity=(round (mul 100 dim_opacity) digits=1)}}
  .dimmed > span { color: color-mix(in hsl, currentColor {{@opacity}}%, transparent); }
  {{! Overlay the default background color with the complementary opacity }}
  .dimmed-bg > span { background: rgb(from {{ palette.colors.black }} r g b / {{round (sub 100 @opacity) digits=1}}%); }
  {{/scope}}
  {{! Only text is concealed, background is not }}
  .concealed { color: rgba(0, 0, 0, 0) !important; }
  {{#if (lt blink.opacity 1)}}
  @keyframes blink {
    0% { color: currentColor; background: transparent; }
    100% {
      color: color-mix(in hsl, currentColor {{round (mul 100 blink.opacity) digits=1}}%, transparent);
      background: rgb(from {{ palette.colors.black }} r g b / {{round (mul 100 (sub 1 blink.opacity)) digits=1}}%);
    }
  }
  .blink > span { animation: {{mul 2 blink.interval}}s steps(2, jump-none) 0s infinite blink; }
  {{/if}}
  {{#if wrap}}
  .hard-br { margin-left: 8px; }
  {{#with wrap.hard_break_at.mark as |mark|}}
  .hard-br:before {
    content: '{{mark}}';
    color: color-mix(in hsl, currentColor 60%, {{ ../palette.colors.black }});
  }
  {{else}}{{/with~}}
  {{~#with line_numbers.continued.mark as |mark|}}
  .line-numbers .cont { margin-right: -3px; }
  .line-numbers .cont:before { content: '{{mark}}'; }
  {{else}}{{/with~}}
  {{~/if~}}
  .line { display: block; }
  .line span { display: inline-block; text-decoration-thickness: 1px; }
  .fg0,.inv.fg-none { color: {{ palette.colors.black }}; } .bg0 { background: {{ palette.colors.black }}; }
  .fg1 { color: {{ palette.colors.red }}; } .bg1 { background: {{ palette.colors.red }}; }
  .fg2 { color: {{ palette.colors.green }}; } .bg2 { background: {{ palette.colors.green }}; }
  .fg3 { color: {{ palette.colors.yellow }}; } .bg3 { background: {{ palette.colors.yellow }}; }
  .fg4 { color: {{ palette.colors.blue }}; } .bg4 { background: {{ palette.colors.blue }}; }
  .fg5 { color: {{ palette.colors.magenta }}; } .bg5 { background: {{ palette.colors.magenta }}; }
  .fg6 { color: {{ palette.colors.cyan }}; } .bg6 { background: {{ palette.colors.cyan }}; }
  .fg7 { color: {{ palette.colors.white }}; } .bg7,.inv.bg-none { background: {{ palette.colors.white }}; }
  .fg8 { color: {{ palette.intense_colors.black }}; } .bg8 { background: {{ palette.intense_colors.black }}; }
  .fg9 { color: {{ palette.intense_colors.red }}; } .bg9 { background: {{ palette.intense_colors.red }}; }
  .fg10 { color: {{ palette.intense_colors.green }}; } .bg10 { background: {{ palette.intense_colors.green }}; }
  .fg11 { color: {{ palette.intense_colors.yellow }}; } .bg11 { background: {{ palette.intense_colors.yellow }}; }
  .fg12 { color: {{ palette.intense_colors.blue }}; } .bg12 { background: {{ palette.intense_colors.blue }}; }
  .fg13 { color: {{ palette.intense_colors.magenta }}; } .bg13 { background: {{ palette.intense_colors.magenta }}; }
  .fg14 { color: {{ palette.intense_colors.cyan }}; } .bg14 { background: {{ palette.intense_colors.cyan }}; }
  .fg15 { color: {{ palette.intense_colors.white }}; } .bg15 { background: {{ palette.intense_colors.white }}; }
</style>
{{/inline~}}

{{~#*inline "content"}}
  {{~#scope line_number=1}}
      <svg{{#if scroll_animation}} id="scroll-container"{{/if}} x="0" y="{{const.WINDOW_PADDING}}" width="{{width}}" height="{{screen_height}}" viewBox="0 0 {{width}} {{screen_height}}" overflow="hidden">
        <foreignObject width="{{width}}" height="{{content_height}}">
          <div xmlns="http://www.w3.org/1999/xhtml" class="container">
            {{~#each interactions}}

            <div class="input{{#if failure}} input-failure{{/if}}{{#if input.hidden}} input-hidden{{/if}}"
              {{~#if (ne exit_status null)}} data-exit-status="{{exit_status}}"{{/if~}}
              {{~#if failure}} title="This command exited with non-zero code"{{/if}}>
              {{~#if ../line_numbers~}}
                {{~#if (and (eq ../line_numbers.scope "continuous") (not input.hidden))~}}
                  <pre class="line-numbers">
                    {{~#each (range 0 (count_lines input.text))~}}
                      {{add this @../../line_number}}{{#if (not @last)}}<br/>{{/if}}
                    {{~/each~}}
                  </pre>
                  {{~set @../line_number (add @../line_number (count_lines input.text))~}}
                {{~/if~}}
              {{~/if~}}
              <pre><span class="prompt">{{ input.prompt }}</span> {{ input.text }}</pre></div>
            {{#if (and (not @first) input.hidden)~}}
              <div class="output-sep"></div>
            {{~/if}}
            <div class="output">
              {{~#if ../line_numbers~}}
                {{~#if (eq ../line_numbers.scope "each_output")~}}
                  {{~set @../line_number 1 ~}}
                {{~/if~}}
                <pre class="line-numbers">
                  {{~#scope skip_line_number=false ~}}
                    {{~#each output as |line|~}}
                      {{~#if (not @../skip_line_number)~}}
                        {{@../../line_number}}
                        {{~set @../../line_number (add 1 @../../line_number)~}}
                      {{~else~}}
                        {{~#if ../../line_numbers.continued.mark}}<b class="cont"></b>{{/if~}}
                      {{~/if~}}
                      {{~#if @last}}{{else}}<br/>{{/if~}}
                      {{~set @../skip_line_number (and line.br (ne ../../line_numbers.continued "inherit"))~}}
                    {{~/each~}}
                  {{~/scope~}}
                </pre>
              {{~/if~}}
              <pre>
              {{~#each output as |line|~}}
                <span class="line">
                {{~#each line.spans as |span|~}}
                  {{~>html_span span~}}
                {{~else~}}{{! The line may be empty }}
                {{~/each~}}
                {{~#if line.br}}<b class="hard-br"></b>{{/if~}}
                {{! Add a newline in order to make the text correctly copyable }}
                {{~#if (not @last)}}

                {{/if~}}
                </span>
              {{~else~}}{{! The output may be empty }}
              {{~/each~}}
              </pre></div>
            {{~/each}}

          </div>
        </foreignObject>
      </svg>
  {{/scope}}
{{/inline~}}

{{~#*inline "main"~}}
{{#scope
  content_height=0
  scroll_animation=null
  screen_height=0
  height=0
  width=width
  line_height=0
}}
  {{~#set @line_height}}{{>compute_line_height}}{{/set~}}
  {{~#debug line_height=@line_height}}computed line height{{/debug~}}

  {{~#set @content_height~}}
    {{~>compute_content_height this line_height=@line_height ~}}
  {{~/set~}}
  {{~set @content_height (round @content_height digits=1)~}}
  {{~#debug content_height=@content_height}}computed content height{{/debug~}}

  {{~#if line_numbers~}}
    {{! Adjust width to take line numbers into account }}
    {{~set @width (add width const.LN_WIDTH const.LN_PADDING)~}}
  {{~/if~}}
  {{~#if scroll~}}
    {{~#set @scroll_animation~}}
      {{~>compute_scroll_animation this width=@width content_height=@content_height~}}
    {{~/set~}}
  {{~/if~}}
  {{~#if @scroll_animation~}}
    {{set @screen_height scroll.max_height}}
  {{~else~}}
    {{set @screen_height @content_height}}
  {{~/if~}}
  {{~set @height (add @screen_height (mul const.WINDOW_PADDING 2))~}}
  {{~#if window~}}
    {{set @height (add @height const.WINDOW_FRAME_HEIGHT)}}
  {{~/if~}}
{{>root (splat_vars this)~}} {{! <-- All rendering happens here }}
{{/scope}}
{{~/inline~}}

{{~>_common~}}