{{!
Computes content height based on line count in interactions.
Expected hash inputs: `interactions`, `const`.
}}
{{~#*inline "compute_content_height"}}
{{#scope lines=0 margins=0 displayed_interactions=0}}
{{#each interactions}}
{{#if (not input.hidden)}}
{{lines set=(add (lines) (count_lines input.text))}}
{{margins set=(add (margins) 1)}}
{{displayed_interactions set=(add (displayed_interactions) 1)}}
{{/if}}
{{lines set=(add (lines) (count_lines output_html format="html"))}}
{{#if (ne 0 (len output_html))}}
{{margins set=(add (margins) 1)}}
{{/if}}
{{/each}}
{{#if (gt (margins) 0)}}
{{! The last margin is not displayed. }}
{{margins set=(sub (margins) 1)}}
{{/if}}
{{add (mul (lines) const.LINE_HEIGHT)
(mul (margins) const.BLOCK_MARGIN)
(mul (displayed_interactions) const.USER_INPUT_PADDING) }}
{{/scope}}
{{/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=(div (sub content_height scroll.max_height) scroll.pixels_per_scroll round="up")
y_step=0
view_box=""
scrollbar_y=""
sep=""
}}
{{y_step set=(div (sub scroll.max_height const.SCROLLBAR_HEIGHT) (steps))}}
{{#each (range 0 (add (steps) 1))}}
{{#sep}}{{#if @first}}""{{else}}";"{{/if}}{{/sep}}
{{#view_box}}"{{view_box}}{{sep}}0 {{mul ../scroll.pixels_per_scroll @index}} {{../width}} {{../scroll.max_height}}"{{/view_box}}
{{#scrollbar_y}}"{{scrollbar_y}}{{sep}}0 {{mul (y_step) @index round="nearest"}}"{{/scrollbar_y}}
{{/each}}
{
"duration": {{mul scroll.interval (steps)}},
"view_box": "{{view_box}}",
"scrollbar_x": {{sub width const.SCROLLBAR_RIGHT_OFFSET}},
"scrollbar_y": "{{scrollbar_y}}"
}
{{/scope}}
{{/if}}
{{/inline~}}
{{! Root template }}
{{~#*inline "root"}}
<!-- Created with {{{creator.name}}} v{{{creator.version}}} ({{{creator.repo}}}) -->
<svg viewBox="0 {{#if window_frame}}-{{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}}
{{/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="{{const.LINE_HEIGHT}}" style="font: 14px {{font_family}}; 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>
{{~#if additional_styles}}
{{{additional_styles}}}
{{~/if}}
.container {
padding: 0 {{const.WINDOW_PADDING}}px;
color: {{ palette.colors.white }};
line-height: {{const.LINE_HEIGHT}}px;
}
.container pre {
padding: 0;
margin: 0;
font: 14px {{font_family}};
line-height: inherit;
}
.input {
{{~#if (eq line_numbers "continuous")}}
display: flex;
{{~/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 (eq line_numbers "continuous")}}
.input > pre { flex-grow: 1; }
{{~/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: 1.5rem;
text-align: right;
padding-right: .5rem;
opacity: 0.35;
user-select: none;
}
{{/if}}
{{~#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}}
.bold,.prompt { font-weight: bold; }
.italic { font-style: italic; }
.underline { text-decoration: underline; }
.dimmed { opacity: 0.7; }
{{~#if wrap}}
.hard-br {
position: relative;
margin-left: 5px;
}
.hard-br:before {
content: '↓';
font-size: 16px;
height: 16px;
position: absolute;
bottom: 0;
transform: rotate(45deg);
opacity: 0.8;
}
{{~/if}}
.fg0 { 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 { 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~}}
{{! Terminal background }}
{{~#*inline "background"}}
<rect width="100%" height="100%" y="{{#if window_frame}}-{{const.WINDOW_FRAME_HEIGHT}}{{else}}0{{/if}}" rx="4.5" style="fill: {{ palette.colors.black }};" />
{{~#if window_frame}}
<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}}
{{/inline~}}
{{~#*inline "content"}}
<svg x="0" y="{{const.WINDOW_PADDING}}" width="{{width}}" height="{{screen_height}}" viewBox="0 0 {{width}} {{screen_height}}">
{{~#if (scroll_animation)}}
{{~#with (scroll_animation)}}
<animate attributeName="viewBox" values="{{view_box}}" dur="{{duration}}s" repeatCount="indefinite" calcMode="discrete" />
{{~/with}}
{{~/if}}
<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 (and (eq ../line_numbers "continuous") (not input.hidden))}}{{>number_input_lines}}{{/if~}}
<pre><span class="prompt">{{ input.prompt }}</span> {{ input.text }}</pre></div>
<div class="output">
{{~#if ../line_numbers~}}
{{~>number_output_lines~}}
{{~#if (ne ../line_numbers "each_output")~}}
{{~line_number set=(add (line_number) (count_lines output_html format="html"))~}}
{{~/if~}}
{{~/if~}}
<pre>{{{output_html}}}</pre></div>
{{~/each}}
</div>
</foreignObject>
</svg>
{{/inline~}}
{{~#*inline "scrollbar"}}
{{#with (scroll_animation)}}
<rect class="scrollbar" x="{{scrollbar_x}}" y="10" width="5" height="40">
<animateTransform attributeName="transform" attributeType="XML" type="translate" values="{{scrollbar_y}}" dur="{{duration}}s" repeatCount="indefinite" calcMode="discrete" />
</rect>
{{/with}}
{{/inline~}}
{{~#*inline "number_input_lines"~}}
<pre class="line-numbers">
{{~#each (range 0 (count_lines input.text))~}}
{{add this (line_number)}}{{#if @last}}{{else}}<br/>{{/if}}
{{~/each~}}
</pre>
{{~line_number set=(add (line_number) (count_lines input.text))~}}
{{~/inline~}}
{{~#*inline "number_output_lines"}}
<pre class="line-numbers">
{{~#each (range 0 (count_lines output_html format="html"))~}}
{{add this (line_number)}}{{#if @last}}{{else}}<br/>{{/if}}
{{~/each~}}
</pre>
{{~/inline~}}
{{! Main logic }}
{{#scope
content_height=(eval "compute_content_height" const=const interactions=interactions)
scroll_animation=null
screen_height=0
height=0
line_number=1
}}
{{~#if scroll~}}
{{scroll_animation set=(eval "compute_scroll_animation"
const=const
scroll=scroll
width=width
content_height=(content_height)
)}}
{{~/if~}}
{{~#if (scroll_animation)~}}
{{screen_height set=scroll.max_height}}
{{~else~}}
{{screen_height set=(content_height)}}
{{~/if~}}
{{~height set=(add (screen_height) (mul const.WINDOW_PADDING 2))~}}
{{~#if window_frame~}}
{{height set=(add (height) const.WINDOW_FRAME_HEIGHT)}}
{{~/if~}}
{{>root~}} {{! <-- All rendering happens here }}
{{/scope}}