Skip to content

Commit f6a470c

Browse files
committed
🖨️ add typst export
1 parent 6fad5ae commit f6a470c

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

frontend/src/editor/export/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { WebVttExportBody } from './webvtt';
77
import { TranscribeeExportBody } from './transcribee';
88
import { ApiDocument } from '../../api/document';
99
import { PlaintextExportBody } from './plaintext';
10+
import { TypstExportBody } from './typst';
1011

1112
export type ExportProps = {
1213
outputNameBase: string;
@@ -34,6 +35,10 @@ const exportTypes: ExportType[] = [
3435
name: 'Plaintext',
3536
component: PlaintextExportBody,
3637
},
38+
{
39+
name: 'Typst',
40+
component: TypstExportBody,
41+
},
3742
{
3843
name: 'Transcribee Archive',
3944
component: TranscribeeExportBody,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as Automerge from '@automerge/automerge';
2+
3+
import { downloadTextAsFile } from '../../utils/download_text_as_file';
4+
import { ExportProps } from '.';
5+
import { PrimaryButton, SecondaryButton } from '../../components/button';
6+
import { Document } from '../../editor/types';
7+
import { formattedTime } from '../transcription_editor';
8+
9+
export function generateTypst(doc: Document): string {
10+
let last_speaker: string | null = null;
11+
const header = `
12+
#let p(time: none, speaker: none, speaker_change: false, body) = {
13+
box(context {
14+
if speaker_change [
15+
#v(1em)
16+
*#speaker:* \
17+
]
18+
let sizeTime = measure(time)
19+
place(dx: -sizeTime.width - 1em, text(fill: gray, time))
20+
body
21+
})
22+
}\n\n`;
23+
24+
return (
25+
header +
26+
doc.children
27+
.map((paragraph) => {
28+
const timeStr = `time: "${formattedTime(paragraph.children[0].start)}"`;
29+
const speakerStr = `speaker: "${
30+
paragraph.speaker && doc.speaker_names[paragraph.speaker]
31+
}"`;
32+
const speakerChangeStr = `speaker_change: ${last_speaker !== paragraph.speaker}`;
33+
let paragraphText = `#p(${timeStr}, ${speakerStr}, ${speakerChangeStr})[\n`;
34+
35+
function escape(text: string): string {
36+
return text
37+
.replace(/\]/g, '\\]')
38+
.replace(/\[/g, '\\[')
39+
.replace(/\*/g, '\\*')
40+
.replace(/_/g, '\\_');
41+
}
42+
43+
paragraphText += paragraph.children
44+
.map((x) => escape(x.text))
45+
.join('')
46+
.trim();
47+
48+
paragraphText += '\n]\n';
49+
if (last_speaker !== paragraph.speaker) {
50+
paragraphText += '\n';
51+
}
52+
53+
last_speaker = paragraph.speaker;
54+
return paragraphText;
55+
})
56+
.filter((x) => x !== '')
57+
.join('\n')
58+
);
59+
}
60+
61+
export function TypstExportBody({ onClose, outputNameBase, editor }: ExportProps) {
62+
return (
63+
<form className="flex flex-col gap-4 mt-4">
64+
<div className="flex justify-between pt-4">
65+
<SecondaryButton type="button" onClick={onClose}>
66+
Cancel
67+
</SecondaryButton>
68+
<PrimaryButton
69+
type="submit"
70+
onClick={async (e) => {
71+
e.preventDefault();
72+
const plaintext = generateTypst(Automerge.toJS(editor.doc));
73+
downloadTextAsFile(`${outputNameBase}.typ`, `text/plain`, plaintext);
74+
onClose();
75+
}}
76+
>
77+
Export
78+
</PrimaryButton>
79+
</div>
80+
</form>
81+
);
82+
}

0 commit comments

Comments
 (0)