Skip to content

Commit b44022d

Browse files
content: Support basic text styles for KaTeX content
1 parent 8fd716e commit b44022d

File tree

4 files changed

+396
-30
lines changed

4 files changed

+396
-30
lines changed

lib/model/content.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,17 +366,20 @@ class MathBlockNode extends BlockContentNode {
366366

367367
class KatexSpanNode extends ContentNode {
368368
const KatexSpanNode({
369+
required this.styles,
369370
required this.text,
370371
required this.nodes,
371372
super.debugHtmlNode,
372373
});
373374

375+
final KatexSpanStyles styles;
374376
final String? text;
375377
final List<KatexSpanNode> nodes;
376378

377379
@override
378380
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
379381
super.debugFillProperties(properties);
382+
properties.add(KatexSpanStylesProperty('styles', styles));
380383
properties.add(StringProperty('text', text));
381384
}
382385

lib/model/katex.dart

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flutter/foundation.dart';
12
import 'package:html/dom.dart' as dom;
23

34
import 'content.dart';
@@ -16,7 +17,205 @@ class KatexParser {
1617
}));
1718
}
1819

20+
static final _resetSizeClassRegExp = RegExp(r'^reset-size(\d\d?)$');
21+
static final _sizeClassRegExp = RegExp(r'^size(\d\d?)$');
22+
1923
KatexSpanNode _parseSpan(dom.Element element) {
24+
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
25+
26+
var styles = KatexSpanStyles();
27+
var index = 0;
28+
while (index < spanClasses.length) {
29+
final spanClass = spanClasses[index];
30+
switch (spanClass) {
31+
case 'textbf':
32+
// .textbf { font-weight: bold; }
33+
styles.fontWeight = KatexSpanFontWeight.bold;
34+
35+
case 'textit':
36+
// .textit { font-style: italic; }
37+
styles.fontStyle = KatexSpanFontStyle.italic;
38+
39+
case 'textrm':
40+
// .textrm { font-family: KaTeX_Main; }
41+
styles.fontFamily = 'KaTeX_Main';
42+
43+
case 'textsf':
44+
// .textsf { font-family: KaTeX_SansSerif; }
45+
styles.fontFamily = 'KaTeX_SansSerif';
46+
47+
case 'texttt':
48+
// .texttt { font-family: KaTeX_Typewriter; }
49+
styles.fontFamily = 'KaTeX_Typewriter';
50+
51+
case 'mathnormal':
52+
// .mathnormal { font-family: KaTeX_Math; font-style: italic; }
53+
styles.fontFamily = 'KaTeX_Math';
54+
styles.fontStyle = KatexSpanFontStyle.italic;
55+
56+
case 'mathit':
57+
// .mathit { font-family: KaTeX_Main; font-style: italic; }
58+
styles.fontFamily = 'KaTeX_Main';
59+
styles.fontStyle = KatexSpanFontStyle.italic;
60+
61+
case 'mathrm':
62+
// .mathrm { font-style: normal; }
63+
styles.fontStyle = KatexSpanFontStyle.normal;
64+
65+
case 'mathbf':
66+
// .mathbf { font-family: KaTeX_Main; font-weight: bold; }
67+
styles.fontFamily = 'KaTeX_Main';
68+
styles.fontWeight = KatexSpanFontWeight.bold;
69+
70+
case 'boldsymbol':
71+
// .boldsymbol { font-family: KaTeX_Math; font-weight: bold; font-style: italic; }
72+
styles.fontFamily = 'KaTeX_Math';
73+
styles.fontWeight = KatexSpanFontWeight.bold;
74+
styles.fontStyle = KatexSpanFontStyle.italic;
75+
76+
case 'amsrm':
77+
// .amsrm { font-family: KaTeX_AMS; }
78+
styles.fontFamily = 'KaTeX_AMS';
79+
80+
case 'mathbb':
81+
case 'textbb':
82+
// .mathbb,
83+
// .textbb { font-family: KaTeX_AMS; }
84+
styles.fontFamily = 'KaTeX_AMS';
85+
86+
case 'mathcal':
87+
// .mathcal { font-family: KaTeX_Caligraphic; }
88+
styles.fontFamily = 'KaTeX_Caligraphic';
89+
90+
case 'mathfrak':
91+
case 'textfrak':
92+
// .mathfrak,
93+
// .textfrak { font-family: KaTeX_Fraktur; }
94+
styles.fontFamily = 'KaTeX_Fraktur';
95+
96+
case 'mathboldfrak':
97+
case 'textboldfrak':
98+
// .mathboldfrak,
99+
// .textboldfrak { font-family: KaTeX_Fraktur; font-weight: bold; }
100+
styles.fontFamily = 'KaTeX_Fraktur';
101+
styles.fontWeight = KatexSpanFontWeight.bold;
102+
103+
case 'mathtt':
104+
// .mathtt { font-family: KaTeX_Typewriter; }
105+
styles.fontFamily = 'KaTeX_Typewriter';
106+
107+
case 'mathscr':
108+
case 'textscr':
109+
// .mathscr,
110+
// .textscr { font-family: KaTeX_Script; }
111+
styles.fontFamily = 'KaTeX_Script';
112+
}
113+
114+
switch (spanClass) {
115+
case 'mathsf':
116+
case 'textsf':
117+
// .mathsf,
118+
// .textsf { font-family: KaTeX_SansSerif; }
119+
styles.fontFamily = 'KaTeX_SansSerif';
120+
121+
case 'mathboldsf':
122+
case 'textboldsf':
123+
// .mathboldsf,
124+
// .textboldsf { font-family: KaTeX_SansSerif; font-weight: bold; }
125+
styles.fontFamily = 'KaTeX_SansSerif';
126+
styles.fontWeight = KatexSpanFontWeight.bold;
127+
128+
case 'mathsfit':
129+
case 'mathitsf':
130+
case 'textitsf':
131+
// .mathsfit,
132+
// .mathitsf,
133+
// .textitsf { font-family: KaTeX_SansSerif; font-style: italic; }
134+
styles.fontFamily = 'KaTeX_SansSerif';
135+
styles.fontStyle = KatexSpanFontStyle.italic;
136+
137+
case 'mainrm':
138+
// .mainrm { font-family: KaTeX_Main; font-style: normal; }
139+
styles.fontFamily = 'KaTeX_Main';
140+
styles.fontStyle = KatexSpanFontStyle.normal;
141+
142+
case 'sizing':
143+
case 'fontsize-ensurer':
144+
// .sizing,
145+
// .fontsize-ensurer { ... }
146+
if (index + 2 < spanClass.length) {
147+
final resetSizeClass = spanClasses[index + 1];
148+
final sizeClass = spanClasses[index + 2];
149+
150+
final resetSizeClassSuffix =_resetSizeClassRegExp.firstMatch(resetSizeClass)?.group(1);
151+
final sizeClassSuffix = _sizeClassRegExp.firstMatch(sizeClass)?.group(1);
152+
153+
if (resetSizeClassSuffix != null && sizeClassSuffix != null) {
154+
const sizes = <double>[0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.2, 1.44, 1.728, 2.074, 2.488];
155+
156+
final resetSizeIdx = int.parse(resetSizeClassSuffix, radix: 10);
157+
final sizeIdx = int.parse(sizeClassSuffix, radix: 10);
158+
159+
// These indexes start at 1.
160+
if (resetSizeIdx <= sizes.length && sizeIdx <= sizes.length) {
161+
styles.fontSizeEm = sizes[resetSizeIdx - 1] * sizes[sizeIdx - 1];
162+
index += 3;
163+
continue;
164+
}
165+
}
166+
}
167+
168+
// Should be unreachable.
169+
throw KatexHtmlParseError();
170+
171+
case 'delimsizing':
172+
// .delimsizing { ... }
173+
if (index + 1 < spanClasses.length) {
174+
final nextClass = spanClasses[index + 1];
175+
switch (nextClass) {
176+
case 'size1':
177+
styles.fontFamily = 'KaTeX_Size1';
178+
case 'size2':
179+
styles.fontFamily = 'KaTeX_Size2';
180+
case 'size3':
181+
styles.fontFamily = 'KaTeX_Size3';
182+
case 'size4':
183+
styles.fontFamily = 'KaTeX_Size4';
184+
}
185+
if (styles.fontFamily == null) throw KatexHtmlParseError();
186+
187+
index += 2;
188+
continue;
189+
}
190+
191+
// Should be unreachable.
192+
throw KatexHtmlParseError();
193+
194+
case 'op-symbol':
195+
// .op-symbol { ... }
196+
if (index + 1 < spanClasses.length) {
197+
final nextClass = spanClasses[index + 1];
198+
switch (nextClass) {
199+
case 'small-op':
200+
styles.fontFamily = 'KaTeX_Size1';
201+
case 'large-op':
202+
styles.fontFamily = 'KaTeX_Size2';
203+
}
204+
if (styles.fontFamily == null) throw KatexHtmlParseError();
205+
206+
index += 2;
207+
continue;
208+
}
209+
210+
// Should be unreachable.
211+
throw KatexHtmlParseError();
212+
213+
// TODO more classes from katex.scss
214+
}
215+
216+
index++;
217+
}
218+
20219
String? text;
21220
List<KatexSpanNode>? spans;
22221
if (element.nodes case [dom.Text(data: final data)]) {
@@ -28,10 +227,91 @@ class KatexParser {
28227

29228
return KatexSpanNode(
30229
text: text,
230+
styles: styles,
31231
nodes: spans ?? const []);
32232
}
33233
}
34234

235+
enum KatexSpanFontWeight {
236+
bold,
237+
}
238+
239+
enum KatexSpanFontStyle {
240+
normal,
241+
italic,
242+
}
243+
244+
enum KatexSpanTextAlign {
245+
left,
246+
center,
247+
right,
248+
}
249+
250+
class KatexSpanStyles {
251+
String? fontFamily;
252+
double? fontSizeEm;
253+
KatexSpanFontStyle? fontStyle;
254+
KatexSpanFontWeight? fontWeight;
255+
KatexSpanTextAlign? textAlign;
256+
257+
KatexSpanStyles({
258+
this.fontFamily,
259+
this.fontSizeEm,
260+
this.fontStyle,
261+
this.fontWeight,
262+
this.textAlign,
263+
});
264+
265+
@override
266+
int get hashCode => Object.hash(
267+
'KatexSpanStyles',
268+
fontFamily,
269+
fontSizeEm,
270+
fontStyle,
271+
fontWeight,
272+
textAlign,
273+
);
274+
275+
@override
276+
bool operator ==(Object other) {
277+
return other is KatexSpanStyles &&
278+
other.fontFamily == fontFamily &&
279+
other.fontSizeEm == fontSizeEm &&
280+
other.fontStyle == fontStyle &&
281+
other.fontWeight == fontWeight &&
282+
other.textAlign == textAlign;
283+
}
284+
285+
static final _zero = KatexSpanStyles();
286+
287+
@override
288+
String toString() {
289+
if (this == _zero) return '${objectRuntimeType(this, 'KatexSpanStyles')}()';
290+
291+
final args = <String>[];
292+
if (fontFamily != null) args.add('fontFamily: $fontFamily');
293+
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
294+
if (fontStyle != null) args.add('fontStyle: $fontStyle');
295+
if (fontWeight != null) args.add('fontWeight: $fontWeight');
296+
if (textAlign != null) args.add('textAlign: $textAlign');
297+
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
298+
}
299+
300+
KatexSpanStyles merge(KatexSpanStyles other) {
301+
return KatexSpanStyles(
302+
fontFamily: other.fontFamily ?? fontFamily,
303+
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
304+
fontStyle: other.fontStyle ?? fontStyle,
305+
fontWeight: other.fontWeight ?? fontWeight,
306+
textAlign: other.textAlign ?? textAlign,
307+
);
308+
}
309+
}
310+
311+
class KatexSpanStylesProperty extends DiagnosticsProperty<KatexSpanStyles> {
312+
KatexSpanStylesProperty(super.name, super.value);
313+
}
314+
35315
class KatexHtmlParseError extends Error {
36316
final String? message;
37317
KatexHtmlParseError([this.message]);

lib/widgets/content.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import '../model/avatar_url.dart';
1515
import '../model/binding.dart';
1616
import '../model/content.dart';
1717
import '../model/internal_link.dart';
18+
import '../model/katex.dart';
1819
import '../model/settings.dart';
1920
import 'code_block.dart';
2021
import 'dialog.dart';
@@ -853,6 +854,7 @@ class _Katex extends StatelessWidget {
853854
textDirection: TextDirection.ltr,
854855
child: DefaultTextStyle(
855856
style: TextStyle(
857+
color: ContentTheme.of(context).textStylePlainParagraph.color,
856858
fontSize: kBaseFontSize * 1.21,
857859
fontFamily: 'KaTeX_Main',
858860
height: 1.2),
@@ -867,6 +869,8 @@ class _KatexSpan extends StatelessWidget {
867869

868870
@override
869871
Widget build(BuildContext context) {
872+
final em = DefaultTextStyle.of(context).style.fontSize!;
873+
870874
Widget widget = const SizedBox.shrink();
871875
if (span.text != null) widget = Text(span.text!);
872876
if (span.nodes.isNotEmpty) {
@@ -879,6 +883,46 @@ class _KatexSpan extends StatelessWidget {
879883
child: _KatexSpan(e));
880884
}))));
881885
}
886+
887+
final styles = span.styles;
888+
TextStyle? textStyle;
889+
TextAlign? textAlign;
890+
891+
if (styles.fontFamily != null) {
892+
textStyle ??= TextStyle();
893+
textStyle = textStyle.copyWith(fontFamily: styles.fontFamily);
894+
}
895+
if (styles.fontSizeEm != null) {
896+
textStyle ??= TextStyle();
897+
textStyle = textStyle.copyWith(fontSize: styles.fontSizeEm! * em);
898+
}
899+
if (styles.fontStyle != null) {
900+
textStyle ??= TextStyle();
901+
textStyle = textStyle.copyWith(fontStyle: switch (styles.fontStyle!) {
902+
KatexSpanFontStyle.normal => FontStyle.normal,
903+
KatexSpanFontStyle.italic => FontStyle.italic,
904+
});
905+
}
906+
if (styles.fontWeight != null) {
907+
textStyle ??= TextStyle();
908+
textStyle = textStyle.copyWith(fontWeight: switch (styles.fontWeight!) {
909+
KatexSpanFontWeight.bold => FontWeight.bold,
910+
});
911+
}
912+
if (styles.textAlign != null) {
913+
textAlign = switch (styles.textAlign!) {
914+
KatexSpanTextAlign.left => TextAlign.left,
915+
KatexSpanTextAlign.center => TextAlign.center,
916+
KatexSpanTextAlign.right => TextAlign.right,
917+
};
918+
}
919+
920+
if (textStyle != null || textAlign != null) {
921+
widget = DefaultTextStyle.merge(
922+
style: textStyle,
923+
textAlign: textAlign,
924+
child: widget);
925+
}
882926
return widget;
883927
}
884928
}

0 commit comments

Comments
 (0)