CaTeX
CaTeX copied to clipboard
Provide an Error callback
Feature description
It would be great to have a direct callback for Errors (eg. of unsupported commands) to handle directly in the build()
function of the parenting Widget. I would like to use this for a fallback to a legacy and slow JavaScript rendering solution for those equations which are not supported.
onError: (e) {
print(e.runtimeType);
print(e);
setState(()=>_useFallbackRendering = true);
}
I would like to use this for the dart package katex_flutter
which is currently using a mix of CaTeX and JS.
👍🏽 this is also our desired use case 🙂
You can already do this in an initState
method for example. The way I would advise you to do it is to copy the CaTeX
widget and replace it with your own implementation, where the parsing catch
statement will fallback to slow rendering 👍🏽
Note that the error handling is not optimal even with this - I hope this is acceptable for a 0.0.1
pre-release 😬
@creativecreatorormaybenot the catch
statement never (really never) catches an error, even if there is unsupported input. Is there any other way to catch the errors? I already tried to add another catch
statement which should catch all errors (including non-CaTeXException
s).
@TheOneWithTheBraid It does catch errors. However, it does not catch configuration and rendering exceptions because they happen at another stage in the build process. This needs to be improved.
If you do not close a group, e.g. {{{ ?
, the catch
block will be triggered.
Is there any way to ~~handle~~ capture rendering errors? Eg. with the ErrorWidget? Or is the ErrorWidget global? If not, a workaround could look like the following dummy code:
ErrorWidget.builder = (FlutterErrorDetails details) {
bool inDebug = false;
assert(() { inDebug = true; return true; }());
// In debug mode, use the normal error widget which shows
// the error message:
if (inDebug)
return ErrorWidget(details.exception);
// In release builds, show a yellow-on-blue message instead:
return Container(
alignment: Alignment.center,
child: LegacyKaTeX(laTeXCode: Text(myUnsupportedLaTeXCode))
);
};
CaTeX(myUnsupportedLaTeXCode);
Whereas LegacyKaTeX
is an implementation of a fallback LaTeX rendering engine.
Do you think that might work?
@TheOneWithTheBraid The error widget is global, but I think this approach could work for now 👌🏼
No, I don't think so: as soon as I use several CaTeX
, I would instantly overwrite the last LaTeX code with the current one.
Is there any update on this? Despite the great concept of this package, I can't use this package for my app unless this issue is resolved. 😢
@rdrgn No, there is no update yet. Note that this will be prioritized once we tackle #62 and hopefully lift this project out of the pre release state 👍
This is super hacky but seems to be working. The fallback here is just a Text()
widget, so replace it with whatever you want instead
import 'package:catex/src/lookup/context.dart';
import 'package:catex/src/lookup/exception.dart';
import 'package:catex/src/lookup/fonts.dart';
import 'package:catex/src/lookup/modes.dart';
import 'package:catex/src/lookup/styles.dart';
import 'package:catex/src/parsing/parsing.dart';
import 'package:catex/src/rendering/rendering.dart';
import 'package:catex/src/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/scheduler.dart';
/// The context that will be passed to the root node in [CaTeX].
///
/// This *does **not** mean* that everything will be rendered
/// using this context. Symbols use different fonts by default.
/// Additionally, there are font functions that modify the font style,
/// color functions that modify the color, and other functions
/// that will need to size their children smaller, e.g. to display a fraction.
/// In general, the context can be modified by any node for its subtree.
final defaultCaTeXContext = CaTeXContext(
// The color and size are overridden using the DefaultTextStyle
// in the CaTeX widget.
color: const Color(0xffffffff),
textSize: 32 * 1.21,
style: CaTeXStyle.d,
fontFamily: CaTeXFont.main.family,
// The weight and style are initialized as null in
// order to be able to override e.g. the italic letter
// behavior using \rm.
);
/// The mode at the root of the tree.
///
/// This can be modified by any node, e.g.
/// a `\text` function will put its subtree into text mode
/// and a `$` will switch to math mode.
/// It simply means that CaTeX will start out in this mode.
const startParsingMode = CaTeXMode.math;
/// Widget that displays TeX using the CaTeX library.
///
/// You can style the base text color and text size using
/// [DefaultTextStyle].
class CustomCaTeX extends StatefulWidget {
/// Constructs a [CaTeX] widget from an [input] string.
const CustomCaTeX(this.input, {Key key})
: assert(input != null),
super(key: key);
/// TeX input string that should be rendered by CaTeX.
final String input;
@override
State createState() => _CaTeXState();
}
class _CaTeXState extends State<CustomCaTeX> {
NodeWidget _rootNode;
Exception exception;
void _parse() {
exception = null;
try {
// ignore: avoid_redundant_argument_values
_rootNode = Parser(widget.input, mode: startParsingMode)
.parse()
.createWidget(defaultCaTeXContext.copyWith(
color: DefaultTextStyle.of(context).style.color,
textSize: DefaultTextStyle.of(context).style.fontSize * 1.21,
));
} on CaTeXException catch (e) {
exception = e;
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_parse();
}
@override
void didUpdateWidget(CustomCaTeX oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.input != widget.input) setState(_parse);
}
@override
Widget build(BuildContext context) {
if (exception != null) {
// Throwing the parsing exception here will make sure that it is
// displayed by the Flutter ErrorWidget.
return Text(widget.input);
}
// Rendering a full tree can be expensive and the tree never changes.
// Because of this, we want to insert a repaint boundary between the
// CaTeX output and the rest of the widget tree.
return _TreeWidget(_rootNode, state: this);
}
}
class _TreeWidget extends SingleChildRenderObjectWidget {
_TreeWidget(
NodeWidget child, {
Key key,
this.state,
}) : assert(child != null),
_context = child.context,
super(child: child, key: key);
final CaTeXContext _context;
final _CaTeXState state;
@override
RenderTree createRenderObject(BuildContext context) =>
CustomRenderTree(_context, state: state);
@override
void updateRenderObject(BuildContext context, RenderTree renderObject) {
renderObject.context = _context;
}
@override
SingleChildRenderObjectElement createElement() =>
CustomSingleChildRenderObjectElement(this, state: state);
}
class CustomSingleChildRenderObjectElement
extends SingleChildRenderObjectElement {
final _CaTeXState state;
CustomSingleChildRenderObjectElement(SingleChildRenderObjectWidget widget,
{this.state})
: super(widget);
@override
void mount(Element parent, dynamic newSlot) {
try {
super.mount(parent, newSlot);
} catch (e) {
SchedulerBinding.instance.addPostFrameCallback((_) {
state.setState(() => state.exception = e);
});
}
}
}
class CustomRenderTree extends RenderTree {
final _CaTeXState state;
CustomRenderTree(CaTeXContext context, {this.state}) : super(context);
@override
void performLayout() {
try {
super.performLayout();
} catch (e) {
SchedulerBinding.instance.addPostFrameCallback((_) {
state.setState(() => state.exception = e);
});
}
}
}
Oh, and you use it with CustomCaTeX(input)
instead of CaTeX(input)
Slightly updated error fallback as there were a few issues with it (when someone tried to render \text{<emoji-here>}
)
// ignore_for_file: implementation_imports, invalid_use_of_protected_member
import 'package:catex/src/lookup/context.dart';
import 'package:catex/src/lookup/exception.dart';
import 'package:catex/src/lookup/fonts.dart';
import 'package:catex/src/lookup/modes.dart';
import 'package:catex/src/lookup/styles.dart';
import 'package:catex/src/parsing/parsing.dart';
import 'package:catex/src/rendering/rendering.dart';
import 'package:catex/src/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/scheduler.dart';
/// The context that will be passed to the root node in [CaTeX].
///
/// This *does **not** mean* that everything will be rendered
/// using this context. Symbols use different fonts by default.
/// Additionally, there are font functions that modify the font style,
/// color functions that modify the color, and other functions
/// that will need to size their children smaller, e.g. to display a fraction.
/// In general, the context can be modified by any node for its subtree.
final defaultCaTeXContext = CaTeXContext(
// The color and size are overridden using the DefaultTextStyle
// in the CaTeX widget.
color: const Color(0xffffffff),
textSize: 32 * 1.21,
style: CaTeXStyle.d,
fontFamily: CaTeXFont.main.family,
// The weight and style are initialized as null in
// order to be able to override e.g. the italic letter
// behavior using \rm.
);
/// The mode at the root of the tree.
///
/// This can be modified by any node, e.g.
/// a `\text` function will put its subtree into text mode
/// and a `$` will switch to math mode.
/// It simply means that CaTeX will start out in this mode.
const startParsingMode = CaTeXMode.math;
final _errorsMap = <String, Exception>{};
/// Widget that displays TeX using the CaTeX library.
///
/// You can style the base text color and text size using
/// [DefaultTextStyle].
class CustomCaTeX extends StatefulWidget {
/// Constructs a [CaTeX] widget from an [input] string.
const CustomCaTeX(this.input, {Key key})
: assert(input != null),
super(key: key);
/// TeX input string that should be rendered by CaTeX.CustomRenderTree
final String input;
@override
State createState() => _CaTeXState();
}
class _CaTeXState extends State<CustomCaTeX> {
NodeWidget _rootNode;
Exception exception;
void _parse() {
exception = null;
try {
// ignore: avoid_redundant_argument_values
_rootNode = Parser(widget.input, mode: startParsingMode)
.parse()
.createWidget(defaultCaTeXContext.copyWith(
color: DefaultTextStyle.of(context).style.color,
textSize: DefaultTextStyle.of(context).style.fontSize * 1.21,
));
} on CaTeXException catch (e) {
exception = e;
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_parse();
}
@override
void didUpdateWidget(CustomCaTeX oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.input != widget.input) setState(_parse);
}
@override
Widget build(BuildContext context) {
exception ??= _errorsMap[widget.input];
if (exception != null) {
_errorsMap[widget.input] = exception;
// Throwing the parsing exception here will make sure that it is
// displayed by the Flutter ErrorWidget.
return Text(widget.input, style: TextStyle(fontFamily: 'monospace'));
}
// Rendering a full tree can be expensive and the tree never changes.
// Because of this, we want to insert a repaint boundary between the
// CaTeX output and the rest of the widget tree.
return _TreeWidget(_rootNode, state: this);
}
}
class _TreeWidget extends SingleChildRenderObjectWidget {
_TreeWidget(
NodeWidget child, {
Key key,
this.state,
}) : assert(child != null),
_context = child.context,
super(child: child, key: key);
final CaTeXContext _context;
final _CaTeXState state;
@override
RenderTree createRenderObject(BuildContext context) =>
CustomRenderTree(_context, state: state);
@override
void updateRenderObject(BuildContext context, RenderTree renderObject) {
renderObject.context = _context;
}
@override
SingleChildRenderObjectElement createElement() =>
CustomSingleChildRenderObjectElement(this, state: state);
}
class CustomSingleChildRenderObjectElement
extends SingleChildRenderObjectElement {
final _CaTeXState state;
CustomSingleChildRenderObjectElement(SingleChildRenderObjectWidget widget,
{this.state})
: super(widget);
@override
void mount(Element parent, dynamic newSlot) {
try {
super.mount(parent, newSlot);
} catch (e) {
SchedulerBinding.instance.addPostFrameCallback((_) {
state.setState(
() => state.exception = e is Exception ? e : Exception(e));
});
}
}
}
class CustomRenderTree extends RenderTree {
final _CaTeXState state;
CustomRenderTree(CaTeXContext context, {this.state}) : super(context);
@override
void performLayout() {
try {
super.performLayout();
} catch (e) {
SchedulerBinding.instance.addPostFrameCallback((_) {
state.setState(
() => state.exception = e is Exception ? e : Exception(e));
});
}
}
}
It is still hacky, a proper error builder would be appropriate
@Sorunome Hi, I agree with you :)
The package development from our side is on-hold at the moment (as you might have noticed).
You can check out https://github.com/znjameswu/flutter_math, which was maintained this whole time - I also contributed a bit over there and we worked together.
We definitely do not want to cancel development of CaTeX, however, our priorities were different ones recently.
didn't know of that, thanks a lot!