CaTeX icon indicating copy to clipboard operation
CaTeX copied to clipboard

Provide an Error callback

Open TheOneWithTheBraid opened this issue 4 years ago • 13 comments

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.

TheOneWithTheBraid avatar Aug 01 '20 10:08 TheOneWithTheBraid

👍🏽 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-CaTeXExceptions).

TheOneWithTheBraid avatar Aug 01 '20 12:08 TheOneWithTheBraid

@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 avatar Aug 01 '20 14:08 TheOneWithTheBraid

@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.

TheOneWithTheBraid avatar Aug 01 '20 16:08 TheOneWithTheBraid

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. 😢

reminjp avatar Oct 01 '20 23:10 reminjp

@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)

Sorunome avatar Oct 21 '20 08:10 Sorunome

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 avatar Nov 20 '20 11:11 Sorunome

@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!

Sorunome avatar Nov 20 '20 12:11 Sorunome