svg-term
Version:
Share terminal sessions via SVG and CSS
204 lines (173 loc) • 5.02 kB
text/typescript
import {flatMap, entries, groupBy, isEqual} from 'lodash';
import hash from 'object-hash';
import { LoadedCast, LoadedFrame } from './load-cast';
import { Theme } from './default-theme';
import { VersionOneFrame, VersionZeroFrame, Line, Cursor, Attributes } from 'load-asciicast';
export interface ViewModelOptions {
cursor?: boolean;
cast: LoadedCast;
from: number;
to: number;
theme: Theme;
height?: number;
}
export interface ViewModel {
displayHeight: number;
displayWidth: number;
duration: number;
frames: ViewFrame[];
height: number;
registry: RegistryItem[];
stamps: number[];
width: number;
}
export interface ViewFrame {
cursor: Cursor;
lines: {
y: number;
words: Word[];
hash: string;
ref: null;
}[];
stamp: number;
}
export interface RegistryItem {
type: string;
words: Word[];
id: number;
}
type AnyFrame = LoadedFrame<VersionOneFrame | VersionZeroFrame>;
export function toViewModel(options: ViewModelOptions): ViewModel {
const {cursor: cursorOption, cast, theme, from, to} = options;
const loadedFrames: AnyFrame[] = cast.frames;
const stamps = loadedFrames
.filter(([stamp]) => stamp >= from && stamp <= to)
.map(([stamp]) => stamp - from);
const fontSize = theme.fontSize;
const lineHeight = theme.lineHeight;
const height = typeof options.height === 'number' ? options.height : cast.height;
const frames = loadedFrames
.filter(([stamp]) => stamp >= from && stamp <= to)
.map(([delta, data]) => [delta, data, liner(data)] as const)
.map(([stamp, data, l]) => {
const lines = l
.map((chars, y) => {
const words = toWords(chars);
return {
y: y * fontSize * lineHeight,
words,
hash: hash(words),
ref: null
};
});
const cursor = getCursor(data);
const cl = lines[cursor.y] || {y: 0};
cursor.x = cursor.x + 2;
cursor.y = Math.max(0, cl.y - 1);
cursor.visible = cursorOption === false ? false : cursor.visible;
return {
cursor,
lines,
stamp: (Math.round(stamp * 100) / 100) - from
};
});
const candidates: (typeof frames[0])["lines"] = flatMap(frames, 'lines').filter(line => line.words.length > 0);
const hashes = groupBy(candidates, 'hash');
const registry = entries(hashes)
.filter(([_, lines]) => lines.length > 1)
.map(([hash, [line]], index) => {
const id = index + 1;
const words = line.words.slice(0);
frames.forEach(frame => {
frame.lines
.filter(line => line.hash === hash)
.forEach(l => {
l.words = [];
(l as any).id = id;
});
});
return {type: 'line', words, id};
});
return {
width: cast.width,
displayWidth: cast.width,
height: cast.height,
displayHeight: height * fontSize * lineHeight,
duration: to - from,
registry,
stamps,
frames
};
}
function getCursor(data: VersionOneFrame | VersionZeroFrame): Cursor {
if (data.hasOwnProperty('cursor')) {
const frame = data as VersionZeroFrame;
return frame.cursor;
}
const frame = data as VersionOneFrame;
return frame.screen.cursor;
}
function liner(data: VersionZeroFrame | VersionOneFrame): Line[] {
if (data.hasOwnProperty('lines')) {
const frame = data as VersionZeroFrame;
return toOne(frame.lines);
}
const frame = data as VersionOneFrame;
return frame.screen.lines;
}
interface Word {
attr: Attributes;
x: number;
children: string;
offset: number;
}
function toWords(chars: Line): Word[] {
return chars
.reduce<Word[]>((words, [point, attr]) => {
if (words.length === 0) {
words.push({
attr,
x: 0,
children: '',
offset: 0
});
}
const word = words[words.length - 1];
const children = String.fromCodePoint(point);
if (children === ' ' && !('bg' in attr) && !attr.inverse) {
word.offset = word.offset + 1;
return words;
}
if (isEqual(word.attr, attr) && word.offset === 0) {
word.children += children;
} else {
words.push({
attr,
x: word.x + word.children.length + word.offset,
children,
offset: 0
});
}
return words;
}, [])
.filter((word) => {
if ('bg' in word.attr || word.attr.inverse) {
return true;
}
const trimmed = word.children.trim();
if ((trimmed === '' || trimmed === '⏎')) {
return false;
}
return true;
});
}
function toOne(arrayLike: any): any {
return Object.entries(arrayLike)
.sort((a: any, b: any) => a[0] - b[0])
.map(e => e[1])
.map((words: any) => words.reduce((chars: any, word: any) => {
const [content, attr] = word;
chars.push(...content.split('').map((char: any) => [char.codePointAt(0), attr]));
return chars;
}, []), []);
}