File: Error.pm

package info (click to toggle)
latexml 0.8.7-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 29,128 kB
  • sloc: xml: 98,982; perl: 29,706; sh: 179; javascript: 28; makefile: 15
file content (1012 lines) | stat: -rw-r--r-- 38,843 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
# /=====================================================================\ #
# |  LaTeXML::Common::Error                                             | #
# | Error handler                                                       | #
# |=====================================================================| #
# | Part of LaTeXML:                                                    | #
# |  Public domain software, produced as part of work done by the       | #
# |  United States Government & not subject to copyright in the US.     | #
# |---------------------------------------------------------------------| #
# | Bruce Miller <bruce.miller@nist.gov>                        #_#     | #
# | http://dlmf.nist.gov/LaTeXML/                              (o o)    | #
# \=========================================================ooo==U==ooo=/ #
package LaTeXML::Common::Error;
use strict;
use warnings;
use LaTeXML::Global;
use LaTeXML::Common::Object;
use LaTeXML::Util::Pathname;
use LaTeXML::Core::Token qw(T_CS);
use Time::HiRes;
use Term::ANSIColor qw(colored colorstrip);

my $IS_WINDOWS;

BEGIN {
  $IS_WINDOWS = $^O eq 'MSWin32';
  require Win32::Console if $IS_WINDOWS;
}

use base qw(Exporter);
our @EXPORT = (
  qw(&SetVerbosity),
  # Managing STDERR and Logfile messages
  qw(&UseSTDERR &UseLog),
  # Error Reporting
  qw(&Fatal &Error &Warn &Info),
  # General messages
  qw(&Note &NoteSTDERR &NoteLog),
  # Progress Spinner
  qw(&ProgressSpinup &ProgressSpindown &ProgressStep),
  # Debugging messages
  qw(&DebuggableFeature &Debug &CheckDebuggable),
  # Colored-logging related functions
  qw(&colorizeString),
  # stateless message generation
  qw(&generateMessage),
  # Status management
  qw(&MergeStatus),
  # Run time reporting
  qw(&StartTime &RunTime),
);

our $VERBOSITY   = 0;
our $IS_TERMINAL = undef;
our $USE_STDERR  = undef;

sub SetVerbosity {
  return $VERBOSITY = $_[0] || 0; }

our $DIE_MESSAGE = "LaTeXML died!\n";    # with cr
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Terminal setup

# Color setup
# Possibly more dynamic?
$Term::ANSIColor::AUTORESET = 1;

# Possibility of more terminal initialization & control?
sub UseSTDERR {
  if (scalar(@_) && !$_[0]) {    # Single false argument? Turn OFF
    _spinnerclear() if $USE_STDERR && $IS_TERMINAL;
    $USE_STDERR  = undef;
    $IS_TERMINAL = undef; }
  else {
    $USE_STDERR  = 1;
    $IS_TERMINAL = -t STDERR;
    binmode(STDERR, ":encoding(UTF-8)");
    use IO::Handle;
    *STDERR->autoflush();

    # Win32 console handling
    if ($IS_WINDOWS && $IS_TERMINAL) {
      # set utf-8 codepage
      # CP_UTF8 = 65001
      Win32::Console::OutputCP(65001);

      # get standard error console
      our $W32_STDERR = Win32::Console->new(&Win32::Console::STD_ERROR_HANDLE());

      # enable VT100 emulation or fall back to ANSI emulation if unsuccessful
      # ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 (not exported by Win32::Console)
      my $mode = $W32_STDERR->Mode();
      unless ($W32_STDERR->Mode($mode | 0x0004) && $W32_STDERR->Mode() & 0x0004) {
        require Win32::Console::ANSI; } } }

  return; }

our %color_scheme = (
  details => 'bold',
  success => 'green',
  info    => 'bright_blue',          # bright only recently defined
  warning => 'yellow',
  error   => 'bold red',
  fatal   => 'bold red underline',
);

sub colorizeString {
  my ($string, $alias) = @_;
  return ($IS_TERMINAL && $color_scheme{$alias}
    ? colored($string, $color_scheme{$alias})
    : $string); }

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Log file
our $LOG;
our $LOG_PATH;
# NOTE: since LaTeXML.pm (currently) repeatedly opens & closes the log,
# and doesn't (YET) know whether there's already a log open,
# this bit of hackery only keeps the outermost log open. FIX THIS!
our $log_count = 0;
# Where? Current directory? (probably) Source directory? (probably not)
# Option for appending?
# Note that the $path can be a reference to a string (which gets appended to)

sub UseLog {
  my ($path, $append) = @_;
  if (!$path) {    # Single false argument? Turn OFF and Close
    $log_count--;
    return if !$LOG || $log_count;
    # ensure trailing newline when flushing, since we may have
    # multiple re-opens during the same conversion run (preamble, main, post ...)
    print $LOG _freshline($LOG);
    close($LOG) or die "Cannot close log file: $!";
    $LOG = undef; }
  else {
    $log_count++;
    return if $LOG or not($path);                 # already opened?
    pathname_mkdir(pathname_directory($path));    # and hopefully no errors! :>
    open($LOG, ($append ? '>>' : '>'), $path) or die "Cannot open log file $path for writing: $!";
    $LOG_PATH = $path;
    binmode($LOG, ":encoding(UTF-8)"); }
  return; }

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Low-level I/O

# print one (or more) lines to the Log, if opened
# Print first line to STDERR if enabled & verbosity >= 0
# Starts a fresh line by pushing any Spinner line ahead.
sub _printline {
  my ($message) = @_;
  return if (!$LOG && !($USE_STDERR && ($VERBOSITY >= 0)));
  $message =~ s/^\n+//s;    # Strip newlines off ends.
  $message =~ s/\n+$//s;
  if (my $clean_message = ($LOG || !$IS_TERMINAL ? strip_ansi($message) : $message)) {
    $message = $clean_message unless $IS_TERMINAL;
    if ($LOG) {
      print $LOG _freshline($LOG), $clean_message, "\n"; }
    # Spinner logic only for terminal-enabled applications
    if ($USE_STDERR && ($VERBOSITY >= 0)) {
      _spinnerclear();
      my $short = $message;
      if ($short =~ /^([^\n]*)(:?\n\s*(at\s+[^\n]*))?/s) {
        my ($first, $more, $at) = ($1, $2, $3);
        $at =~ s/\s+-\s+.*$// if $at;
        $short = $first;
        $short .= ' ' . $at if $at; }
      print STDERR _freshline(\*STDERR), $short, "\n";    ##}
      _spinnerrestore(); } }
  return; }

# Similar, but print ALL lines to STDERR as well.
sub _printlines {
  my ($message) = @_;
  return if (!$LOG && !($USE_STDERR && ($VERBOSITY >= 0)));
  $message =~ s/^\n+//s;    # Strip newlines off ends.
  $message =~ s/\n+$//s;
  if (my $clean_message = ($LOG || !$IS_TERMINAL ? strip_ansi($message) : $message)) {
    $message = $clean_message unless $IS_TERMINAL;
    if ($LOG) {
      print $LOG _freshline($LOG), $clean_message, "\n"; }
    # Spinner logic only for terminal-enabled applications
    if ($USE_STDERR && ($VERBOSITY >= 0)) {
      _spinnerclear();
      print STDERR _freshline(\*STDERR), $message, "\n";    ##}
      _spinnerrestore(); } }
  return; }

our %NEEDSFRESHLINE = ();

sub _freshline {
  my ($stream) = @_;
  if ($stream && $NEEDSFRESHLINE{$stream}) {
    $NEEDSFRESHLINE{$stream} = 0;
    return "\n"; }
  return ''; }

sub strip_ansi {
  my ($string) = @_;
  $string =~ s/\e\[[0-9;]*[a-zA-Z]//g;
  return $string; }

#======================================================================
sub StartTime {
  return [Time::HiRes::gettimeofday]; }

sub RunTime {
  my ($starttime) = @_;
  my $s = Time::HiRes::tv_interval($starttime, [Time::HiRes::gettimeofday]);
  my ($h, $m);
  $m = int($s / 60); $s -= 60 * $m;
  $h = int($m / 60); $m -= 60 * $h;
  return ($h ? $h . 'h ' : '') . ($m ? $m . 'm ' : '') . sprintf("%.2fs", $s); }

#======================================================================
# Spinner support
# Stack of [stage,count,count_message]
# Note: Would look prettier if we blank the cursor, but have to restore!
# Note: linewrap leaves terminal turds: the disable/enable codes are VT escape codes
our @spinnerstack = ();
our @spinnerchar  = map { colored($_, "bold red"); } ('-', '\\', '|', '/');
our $spinnerpos   = 0;
our $spinnerpre   = "\x1b[1G\x1b[?7l";    # Cursor to col 1; turn off linewrap
our $spinnerpost  = "\x1b[?7h";
# sub _spinnerreset {
#   if($USE_STDERR && $IS_TERMINAL){
#     print STDERR "\x1b[?7h"; }  # Reset linewrap on
#   return; }

sub _spinnerclear {    # Clear the spinner line (if any)
  if ($USE_STDERR && $IS_TERMINAL && ($VERBOSITY >= 0) && @spinnerstack) {
    print STDERR "\x1b[1G\x1b[0K"; }    # clear line
  return; }

sub _spinnerrestore {    # Restore the spinner line (if any)
  if ($USE_STDERR && $IS_TERMINAL && ($VERBOSITY >= 0) && @spinnerstack) {
    my ($stage, $short, $start) = @{ $spinnerstack[-1] };
    print STDERR join(' ', $spinnerpre, $spinnerchar[$spinnerpos],
      (map { $$_[1]; } @spinnerstack[0 .. $#spinnerstack - 1]), $stage), $spinnerpost; }
  return; }

sub _spinnerstep {    # Increment stepper
  my ($note) = @_;
  if ($USE_STDERR && $IS_TERMINAL && ($VERBOSITY >= 0) && @spinnerstack) {
    my ($stage, $short, $start) = @{ $spinnerstack[-1] };
    $spinnerpos = ($spinnerpos + 1) % 4;
    if ($note) {    # If note, redraw whole line.
      print STDERR join(' ', $spinnerpre, $spinnerchar[$spinnerpos],
        (map { $$_[1]; } @spinnerstack), $note, "\x1b[0K"), $spinnerpost; }
    else {          # overwrite previous spinner
      print STDERR $spinnerpre . ' ', $spinnerchar[$spinnerpos], $spinnerpost; } }
  return; }

sub _spinnerpush {    # New spinner level
  my ($stage) = @_;
  my $short = ($stage =~ /^(\w+)\s+(.*)$/ && $2 ? "$1 >" : $stage);
  push(@spinnerstack, [$stage, $short, [Time::HiRes::gettimeofday]]);
  return; }

sub _spinnerpop {    # Finished with spinner level
  my ($stage) = @_;
  if (@spinnerstack && ($stage eq $spinnerstack[-1][0])) {
    my ($xstage, $short, $start) = @{ pop(@spinnerstack) };
    return Time::HiRes::tv_interval($start, [Time::HiRes::gettimeofday]); }
  elsif ($USE_STDERR && ($VERBOSITY >= 0)) {    # What else to do about mis-matched begin/end ??
    print STDERR "SPINNER is " . ((@spinnerstack && $spinnerstack[-1][0]) || 'undef') . " not $stage\n"; }
  return; }

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Error reporting
# Public API

sub Fatal {
  my ($category, $object, $where, $message, @details) = @_;

# Check if this is a known unsafe fatal and flag it if so (so that we reinitialize in daemon contexts)
  if ((($category eq 'internal') && ($object eq '<recursion>')) ||
    ($category eq 'too_many_errors') ||
    ($object eq 'deep_recursion')    || ($object eq 'die')) {
    $LaTeXML::UNSAFE_FATAL = 1; }

  # We'll assume that if the DIE handler is bound (presumably to this function)
  # we're in the outermost call to Fatal; we'll clear the handler so that we don't nest calls.
  die $DIE_MESSAGE if $LaTeXML::IGNORE_ERRORS    # Short circuit, w/no formatting, if in probing eval
    || (($SIG{__DIE__} eq 'DEFAULT') && $^S);    # Also missing class when parsing bindings(?!?!)

  my $inhandler = !$SIG{__DIE__};
  my $ineval    = 0;                             # whether we're in an eval should no longer matter!

  # This seemingly should be "local", but that doesn't seem to help with timeout/alarm/term?
  # It should be safe so long as the caller has bound it and rebinds it if necessary.
  local $SIG{__DIE__} = 'DEFAULT';    # Avoid recursion while preparing the message.
  my $state = $STATE;

  if (!$inhandler) {
    local $LaTeXML::BAILOUT = $LaTeXML::BAILOUT;
    if (checkRecursiveError()) {
      $LaTeXML::BAILOUT = 1;
      push(@details, "Recursive Error!"); }
    $state->noteStatus('fatal') if $state && !$ineval;
    my $detail_level = (($VERBOSITY <= 1) && ($category =~ /^(?:timeout|too_many_errors)$/)) ? 0 : 2;
    $message
      = generateMessage(colorizeString("Fatal:" . $category . ":" . ToString($object), 'fatal'),
      $where, $message, $detail_level, @details);
    # If we're about to (really) DIE, we'll bypass the usual status message, so add it here.
    # This really should be handled by the top-level program,
    # after doing all processing within an eval
    # BIZARRE: Note that die adds the "at <file> <line>" stuff IFF the message doesn't end w/ CR!
####    $message .= $state->getStatusMessage . "\n" if $state && !$ineval;
  }
  else {    # If we ARE in a recursive call, the actual message is $details[0]
    $message = $details[0] if $details[0]; }
  # inhibit message to STDERR, since die will handle that
  _printlines($message);
  hardYankProcessing();
  # Now that we have yanked the processing state, ignore any following errors
  $LaTeXML::IGNORE_ERRORS = 1;

  # If inside an eval, this won't actually die, but WILL set $@ for caller's use.
  die $DIE_MESSAGE; }

sub hardYankProcessing {
  my $state = $STATE;
  # Nothing we can do if we are called without a global $STATE bound
  return unless $state;
  # Ensure we have nothing else to do in the main processing.
  # NOTE: this recovery procedure must always be run after all logging messages are generated,
  #       as resetting the various stacks loses information (e.g. location is lost).
  my $stomach = $$state{stomach};
  my $gullet  = $$stomach{gullet};
  $$stomach{token_stack} = [];
  # If we were in an infinite loop, disable any potential busy token.
  my $relax_def = $$state{meaning}{"\\relax"}[0];
  $state->assignMeaning($LaTeXML::CURRENT_TOKEN, $relax_def, 'global') if $LaTeXML::CURRENT_TOKEN;
  for my $token (@{ $$gullet{pushback} }) {
    $state->assignMeaning($token, $relax_def, 'global'); }
  # Rescue data structures that may be serializable/resumable
  if (@LaTeXML::LIST) {
    $$stomach{rescued_boxes} = [@LaTeXML::LIST];
    @LaTeXML::LIST = ();
  }
  if ($LaTeXML::DOCUMENT) {
    $$state{rescued_document} = $LaTeXML::DOCUMENT; }
  # avoid looping at \end{document}, Fatal brings us back to the doc level
  $state->assignValue('current_environment', undef, 'global');
  # then reset the gullet
  $$gullet{pushback}         = [];
  $$gullet{mouthstack}       = [];
  $$gullet{pending_comments} = [];
  $$gullet{mouth}            = LaTeXML::Core::Mouth->new();
  return; }

sub checkRecursiveError {
  my @caller;
  for (my $frame = 2 ; @caller = caller($frame) ; $frame++) {
    if ($caller[3] =~ /^LaTeXML::(Global::ToString|Global::Stringify)$/) {
      #      print STDERR "RECURSED ON $caller[3]\n";
      return 1; } }
  return; }

# Should be fatal if strict is set, else warn.
sub Error {
  my ($category, $object, $where, $message, @details) = @_;
  return if $LaTeXML::IGNORE_ERRORS;
  my $state = $STATE;
  $state && $state->noteStatus('error');
  if ($state && $state->lookupValue('STRICT')) {
    Fatal($category, $object, $where, $message, @details); }
  else {
    my $formatted = generateMessage("Error:" . $category . ":" . ToString($object),
      $where, $message, 1, @details);
    _printline($formatted); }
  # Note that "100" is hardwired into TeX, The Program!!!
  my $maxerrors = ($state ? $state->lookupValue('MAX_ERRORS') : 100);
  if ($state && (defined $maxerrors) && (($state->getStatus('error') || 0) > $maxerrors)) {
    Fatal('too_many_errors', $maxerrors, $where, "Too many errors (> $maxerrors)!"); }
  return; }

# Warning message; results may be OK, but somewhat unlikely
sub Warn {
  my ($category, $object, $where, $message, @details) = @_;
  return if $LaTeXML::IGNORE_ERRORS;
  my $state = $STATE;
  $state && $state->noteStatus('warning');
  my $formatted = generateMessage("Warning:" . $category . ":" . ToString($object),
    $where, $message, 0, @details);
  _printline($formatted);
  return; }

# Informational message; results likely unaffected
# but the message may give clues about subsequent warnings or errors
sub Info {
  my ($category, $object, $where, $message, @details) = @_;
  return if $LaTeXML::IGNORE_ERRORS;
  my $state = $STATE;
  $state && $state->noteStatus('info');
  my $formatted = generateMessage("Info:" . $category . ":" . ToString($object),
    $where, $message, -1, @details);
  _printline($formatted);
  return; }

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Progress Reporting
#**********************************************************************

sub Note {
  my ($message) = @_;
  _printline($message);
  return; }

sub NoteSTDERR {
  my ($message) = @_;
  if ($USE_STDERR && ($VERBOSITY >= 0)) {
    _spinnerclear();
    print STDERR _freshline(\*STDERR), $message, "\n";
    _spinnerrestore(); }
  return; }

sub NoteLog {
  my ($message) = @_;
  print $LOG _freshline($LOG), strip_ansi($message), "\n" if $LOG;
  return; }

# Progress reporting.
# Needs LOG/STDERR sorted out. Maybe some Term magic on STDERR? (rotating "-"?)
# Possibly wants more explicit levels?
# or at least a report-always level?
sub ProgressStep {
  my ($note) = @_;
  _spinnerstep($note);
  return; }

sub ProgressSpinup {
  my ($stage) = @_;
  if ($LOG || ($USE_STDERR && ($VERBOSITY >= 0))) {
    my $message = "($stage...";
    _spinnerclear();
    _spinnerpush($stage);
    _spinnerrestore();
    if ($LOG) {
      print $LOG _freshline($LOG), $message;
      $NEEDSFRESHLINE{$LOG} = 1 if $LOG; }
    if ($USE_STDERR && ($VERBOSITY >= 0) && !$IS_TERMINAL) {
      print STDERR _freshline(\*STDERR), $message;
      $NEEDSFRESHLINE{ \*STDERR } = 1; } }
  return; }

sub ProgressSpindown {
  my ($stage) = @_;
  if ($LOG || ($USE_STDERR && ($VERBOSITY >= 0))) {
    _spinnerclear();
    my $elapsed = _spinnerpop($stage);
    _spinnerrestore();
    my $message = ($elapsed ? sprintf(" %.2f sec)", $elapsed) : '?');
    print $LOG $message       if $LOG;
    $NEEDSFRESHLINE{$LOG} = 1 if $LOG;
    if ($USE_STDERR && ($VERBOSITY >= 0) && !$IS_TERMINAL) {
      print STDERR $message;
      $NEEDSFRESHLINE{ \*STDERR } = 1; } }
  return; }

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Debugging support.
# Short of real macros, here's a flexible, low-cost debug technique:
#   Debug(message...) if $LaTeXML::DEBUG{feature};
our %Debugbable = ();
#  %LaTeXML::DEBUG      = {};

sub DebuggableFeature {
  my ($feature, $description) = @_;
  $LaTeXML::Debuggable{$feature} = $description;
  return; }

sub Debug {
  my ($message) = @_;
  # Note: Could append source code location of the caller?
  _printlines($message);
  return; }

# This only makes sense at end of run, after all needed modules have been loaded!
sub CheckDebuggable {
  my %unknown = ();
  foreach my $feature (keys %LaTeXML::DEBUG) {
    $unknown{$feature} = 1 unless $LaTeXML::Debuggable{$feature}; }
  # Now report unknown; suggest similar spellings ?
  if (keys %unknown) {
    print STDERR _freshline(\*STDERR), "The debugging feature(s) " . join(', ', sort keys %unknown) . " were never declared\n";
    print STDERR _freshline(\*STDERR), "Known debugging features: " . join(', ', sort keys %LaTeXML::Debuggable) . "\n"; }
  return; }

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Handlers for perl's die & warn
# We'll try to decode some common errors to make them more usable
# for build systems.

my $quoted_re     = qr/\"([^\"]*)\"/;                                                   # [CONSTANT]
my $cantcall_re   = qr/Can't call method/;                                              # [CONSTANT]
my $cantlocate_re = qr/Can't locate object method/;                                     # [CONSTANT]
my $undef_re      = qr/Undefined subroutine/;                                           # [CONSTANT]
my $noself_re     = qr/on an undefined value|without a package or object reference/;    # [CONSTANT]
my $via_re        = qr/via package/;                                                    # [CONSTANT]
my $at_re         = qr/(at .*)/;                                                        # [CONSTANT]

sub perl_die_handler {
  my (@line) = @_;
  if ($LaTeXML::IGNORE_ERRORS    # Just get out now, if we're ignoring errors within an eval.
    || (colorstrip($line[0]) =~ /^\s*Fatal:/)) {    # Or, we've already been through here.
    local $SIG{__DIE__} = undef;
    die @line; }
  # We try to find a meaningful name for where the error occurred;
  # That's the thing that is "misdefined", after all.
  # Not completely sure we're looking in the right place up the stack, though.
  if ($line[0] =~ /^$cantcall_re\s+$quoted_re\s+($noself_re)\s+$at_re$/) {
    my ($method, $kind, $where) = ($1, $2, $3);
    Fatal('misdefined', callerName(1), $where,
      "Can't call method '$method' $kind", @line[1 .. $#line]); }
  elsif ($line[0] =~ /^$undef_re\s+(\S+)\s+called $at_re$/) {
    my ($function, $where) = ($1, $2);
    Fatal('misdefined', callerName(1), $where,
      "Undefined subroutine '$function' called", @line[1 .. $#line]); }
  elsif ($line[0] =~ /^$cantlocate_re\s+$quoted_re\s+$via_re\s+$quoted_re\s+\(.*\)\s+$at_re/) {
    my ($method, $class, $where) = ($1, $2, $3);
    Fatal('misdefined', callerName(1), $where,
      "Can't locate method '$method' via '$class'", @line[1 .. $#line]); }
  elsif ($line[0] =~ /^Can't locate \S* in \@INC \(you may need to install the (\S*) module\) \(\@INC contains: ([^\)]*)\) $at_re$/) {
    my ($class, $inc, $where) = ($1, $2);
    Fatal('misdefined', callerName(1), $where,
      "Can't locate class '$class' (not installed or misspelled?)", @line[1 .. $#line]); }
  elsif ($line[0] =~ /^Can't use\s+(\w*)\s+\([^\)]*\) as (.*?) ref(?:\s+while "strict refs" in use)? at (.*)$/) {
    my ($gottype, $wanttype, $where) = ($1, $2, $3);
    Fatal('misdefined', callerName(1), $where,
      "Can't use $gottype as $wanttype reference", @line[1 .. $#line]); }
  elsif ($line[0] =~ /^File (.*?) had an error:/) {
    my ($file) = ($1);
    Fatal('misdefined', $file, undef, @line); }
  else {
    Fatal('perl', 'die', undef, "Perl died", @line); }
  return; }

sub perl_warn_handler {
  my (@line) = @_;
  return if $LaTeXML::IGNORE_ERRORS;
  if ($line[0] =~ /^Use of uninitialized value (.*?)(\s?+in .*?)\s+(at\s+.*?\s+line\s+\d+)\.$/) {
    my ($what, $how, $where) = ($1 || 'value', $2, $3);
    Warn('uninitialized', $what, $where, "Use of uninitialized value $what $how", @line[1 .. $#line]); }
  elsif ($line[0] =~ /^Deep recursion on/) {
    Fatal('perl', 'deep_recursion', undef, $line[0]); }
  elsif ($line[0] =~ /^(.*?)\s+(at\s+.*?\s+line\s+\d+)\.$/) {
    my ($warning, $where) = ($1, $2);
    Warn('perl', 'warn', undef, $warning, $where, @line[1 .. $#line]); }
  else {
    Warn('perl', 'warn', undef, @line); }
  return; }

# The following handlers SHOULD report the problem,
# even when within a "safe" eval that's ignoring errors.
# Moreover, we'd really like to be able to throw all the way to
# the top-level containing eval.  How to do that?
sub perl_interrupt_handler {
  my (@line) = @_;
  $LaTeXML::IGNORE_ERRORS = 0;    # NOT ignored
  $LaTeXML::UNSAFE_FATAL  = 1;
  Fatal('interrupt', 'interrupted', undef, "LaTeXML was interrupted", @line);
  return; }

sub perl_timeout_handler {
  my (@line) = @_;
  $LaTeXML::IGNORE_ERRORS = 0;    # NOT ignored
  $LaTeXML::UNSAFE_FATAL  = 1;
  Fatal('timeout', 'timedout', undef, "Conversion timed out", @line);
  return; }

sub perl_terminate_handler {
  my (@line) = @_;
  $LaTeXML::IGNORE_ERRORS = 0;    # NOT ignored
  $LaTeXML::UNSAFE_FATAL  = 1;
  Fatal('terminate', 'terminated', undef, "Conversion was terminated", @line);
  return; }

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Internals
# Synthesize an error message describing what happened, and where.
# $detail specifies the level of detail
#   $detail == -1 : no context or stack
#   $detail == 0  : context, no stack
#   $detail == +1 : context & stack
# including a level requesting full stack trace?

sub generateMessage {
  my ($errorcode, $where, $message, $detail, @extra) = @_;
  # Colorize errorcode if appropriate
  if ($USE_STDERR && $IS_TERMINAL && ($VERBOSITY >= 0)) {
    $errorcode =~ /^(\w+)\:/;
    my $errorkind = $1 && lc($1);
    $errorcode = colorizeString($errorcode, $errorkind) if $errorkind; }

  #----------------------------------------
  # Generate location information; basic and for stack trace.
  # If we've been given an object $where, where the error occurred, use it.
  my $docloc = getLocation($where);
  $docloc = defined $docloc ? ToString($docloc) : '';

  # $message and each of @extra should be single lines
  @extra = grep { $_ ne '' } map { split("\n", $_) } grep { defined $_ } $message, @extra;
  # make 1st line be 1st line of message
  $message = shift(@extra);
  #  $message =~ s/\n.*//g;
  # The initial portion of the message will consist of:
  $message = '' unless defined $message;
  my @lines = (
    # Start with the error code & primary error message
    $errorcode . ' ' . $message,
    # Followed by single line location of where the message occurred (if we know)
    ($docloc ? ('at ' . $docloc) : ()),
    # and then any additional message lines supplied
    @extra);

  #----------------------------------------
  # Now add some additional context
  # NOTE: Should skip this for INFO
  # NOTE: Need to pass more of this onto the objects themselves....
  # What should it be called?
  #   showErrorContext() ?????
  $detail = 0 unless defined $detail;
  # Increment $detail if $verbosity > 0, unless $detail = -1,
  if (($detail > -1) && ($VERBOSITY > 0)) {
    $detail = 0 if defined $VERBOSITY && $VERBOSITY < -1;
    $detail++ if defined $VERBOSITY && $VERBOSITY > +1; }

  # FIRST line of stack trace information ought to look at the $where
  my $wheretype = ref $where;
  if    ($detail <= 0) { }                 # No extra context
  elsif ($wheretype =~ /^XML::LibXML/) {
    push(@lines, "Node is " . Stringify($where)); }
  ## Hmm... if we're being verbose or level is high, we might do this:
  ### "Currently in ".$doc->getInsertionContext); }
  elsif ($wheretype =~ 'LaTeXML::Core::Gullet') {
    push(@lines, $where->showUnexpected); }    # Or better?
  elsif ($wheretype =~ 'LaTeXML::Core::Stomach') {
    push(@lines,
      "Recently digested: " . join(' ', map { Stringify($_) } @LaTeXML::LIST))
      if $VERBOSITY > 1; }

  #----------------------------------------
  # Add Stack Trace, if that seems worthwhile.
  if (($detail > 1) && ($VERBOSITY > 0)) {
    push(@lines, "Stack Trace:", stacktrace()); }
  elsif ($detail > -1) {
    my $nstack = ($detail > 1 ? undef : ($detail > 0 ? 4 : 1));
    if (my @objects = objectStack($nstack)) {
      my $top = shift(@objects);
      push(@lines,   "In " . trim(Stringify($$top[0])) . ' ' . Stringify($$top[1]));
      push(@objects, ['...']) if @objects && defined $nstack;
      push(@lines,   join('', (map { ' <= ' . trim(Stringify($$_[0])) } @objects))) if @objects;
  } }

  # finally, join the result into a block of lines, indenting all but the 1st line.
  return join("\n\t", @lines); }

sub MergeStatus {
  my ($external_state) = @_;
  my $state = $STATE;
  return unless $state && $external_state;
  my $status          = $$state{status};
  my $external_status = $$external_state{status};
  # Should this be a state method? I suspect XS-ive conflicts later on...
  foreach my $type (keys %$external_status) {
    if ($type eq 'undefined' or $type eq 'missing') {
      my $table = $$external_status{$type};
      foreach my $subtype (keys %$table) {
        $$status{$type}{$subtype} += $$table{$subtype};
      }
    } else {
      $$status{$type} += $$external_status{$type};
    }
  }
  return; }

# returns the locator of an object, or undef
sub Locator {
  my ($object) = @_;
  return ($object && $object->can('getLocator') ? $object->getLocator : undef); }

# A more organized abstraction along there likes of $where->whereAreYou
# might be useful?
sub getLocation {
  my ($where) = @_;
  my $wheretype = ref $where;
  if ($wheretype && ($wheretype =~ /^XML::LibXML/)) {
    my $box = $LaTeXML::DOCUMENT && $LaTeXML::DOCUMENT->getNodeBox($where);
    return Locator($box) if $box; }
  elsif ($wheretype && $where->can('getLocator')) {
    return $where->getLocator; }
  elsif (defined $where) {
    return $where; }
  # Otherwise, try to guess where the error came from!
  elsif ($LaTeXML::DOCUMENT) {    # During construction?
    my $node = $LaTeXML::DOCUMENT->getNode;
    my $box  = $LaTeXML::DOCUMENT->getNodeBox($node);
    return Locator($box) if $box; }
  if ($LaTeXML::BOX) {            # In constructor?
    return Locator($LaTeXML::BOX); }
  if ($STATE && $STATE->getStomach) {
    my $gullet = $STATE->getStomach->getGullet;
    # NOTE: Problems here.
    # (1) With obsoleting Tokens as a Mouth, we can get pointless "Anonymous String" locators!
    # (2) If gullet is the source, we probably want to include next token, etc or
    return Locator($gullet); }
  # # If in postprocessing
  # if($LaTeXML::Post::PROCESSOR && $LaTeXML::Post::DOCUMENT){
  #   return 'in '. $LaTeXML::Post::PROCESSOR->getName
  #     . ' on '. $LaTeXML::Post::DOCUMENT->siteRelativeDestination; }
  return; }

sub callerName {
  my ($frame) = @_;
  my %info = caller_info(($frame || 0) + 2);
  return $info{sub}; }

sub callerInfo {
  my ($frame) = @_;
  my %info = caller_info(($frame || 0) + 2);
  return "$info{call} @ $info{file} line $info{line}"; }

#======================================================================
# This portion adapted from Carp; simplified (but hopefully still correct),
# allow stringify overload, handle methods, make more concise!
#======================================================================
my $MAXARGS = 8;     # [CONSTANT]
my $MAXLEN  = 40;    # Or more? [CONSTANT]

sub trim {
  my ($string) = @_;
  return $string unless defined $string;
  $string = substr($string, 0, $MAXLEN - 3) . "..." if (length($string) > $MAXLEN);
  $string =~ s/\n/\x{240D}/gs;    # symbol for CR
  return $string; }

sub caller_info {
  my ($i) = @_;

  my (%info, @args);
  { package DB;
    @info{qw(package file line sub has_args wantarray evaltext is_require)}
      = caller($i);
    @args = @DB::args; }
  return () unless defined $info{package};
  # Work out the effective sub name, or eval, or method ...
  my $call = '';
  if (defined $info{evaltext}) {
    my $eval = $info{evaltext};
    if ($info{is_require}) {
      $call = "require $eval"; }
    else {
      $eval =~ s/([\\\'])/\\$1/g;
      $call = "eval '" . trim($eval) . "'"; } }
  elsif ($info{sub} eq '(eval)') {
    $call = "eval {...}"; }
  else {
    $call = $info{sub};
    my $method = $call;
    $method =~ s/^.*:://;
    # If $arg[0] is blessed, and `can' do $method, then we'll guess it's a method call?
    if ($info{has_args} && @args
      && ref $args[0] && ((ref $args[0]) !~ /^(?:SCALAR|ARRAY|HASH|CODE|REF|GLOB|LVALUE)$/)
      && $args[0]->can($method)) {
      $call = format_arg(shift(@args)) . "->" . $method; } }
  # Append arguments, if any.
  if ($info{has_args}) {
    @args = map { format_arg($_) } @args;
    if (@args > $MAXARGS) {
      $#args = $MAXARGS; push(@args, '...'); }
    $call .= '(' . join(',', @args) . ')'; }
  $info{call} = $call;
  return %info; }

sub format_arg {
  my ($arg) = @_;
  if    (not defined $arg)      { $arg = 'undef'; }
  elsif (ref $arg)              { $arg = Stringify($arg); }    # Allow overloaded stringify!
  elsif ($arg =~ /^-?[\d.]+\z/) { }                            # Leave numbers alone.
  else {                                                       # Otherwise, string, so quote
    $arg =~ s/'/\\'/g;                                         # Slashify '
    $arg =~ s/([[:cntrl:]])/ "\\".chr(ord($1)+ord('A'))/ge;
    $arg = "'$arg'" }
  return trim($arg); }

# Semi-traditional (but reformatted) stack trace
sub stacktrace {
  my $frame = 0;
  my $trace = "";
  while (my %info = caller_info($frame++)) {
    next if $info{sub} =~ /^LaTeXML::Common::Error/;
##    $info{call} = '' if $info{sub} =~ /^LaTeXML::Common::Error::(?:Fatal|Error|Warn|Info)/;
    $trace .= "\t$info{call} @ $info{file} line $info{line}\n"; }
  return $trace; }

# Extract blessed `interesting' objects on stack.
# Get a maximum of $maxdepth objects (if $maxdepth is defined).
sub objectStack {
  my ($maxdepth) = @_;
  my $frame      = 0;
  my @objects    = ();
  while (1) {
    my (%info, @args);
    { package DB;
      @info{qw(package file line sub has_args wantarray evaltext is_require)} = caller($frame++);
      @args = @DB::args; }
    last unless defined $info{package};
    next if ($info{sub} eq '(eval)') || !$info{has_args} || !@args;
    my $self = $args[0];
    # If $arg[0] is blessed, and `can' do $method, then we'll guess it's a method call?
    # We'll collect such objects provided they can ->getLocator
    if ((ref $self) && ((ref $self) !~ /^(?:SCALAR|ARRAY|HASH|CODE|REF|GLOB|LVALUE)$/)) {
      my $method = $info{sub};
      $method =~ s/^.*:://;
      if ($self->can($method)) {
        next if @objects && ($self eq $objects[-1][0]);    # but don't duplicate
        if ($self->can('getLocator')) {                    # Digestion object?
          push(@objects, [$self, Locator($self)]); }
        elsif ($self->isa('LaTeXML::Post::Processor') || $self->isa('LaTeXML::Post::Document')) {
          push(@objects, [$self, '->' . $method]); }
        last if $maxdepth && (scalar(@objects) >= $maxdepth); } } }
  return @objects; }

#**********************************************************************
1;

__END__

=pod

=head1 NAME

C<LaTeXML::Common::Error> - Error and Progress Reporting and Logging support.

=head1 DESCRIPTION

C<LaTeXML::Common::Error> does some simple stack analysis to generate more informative, readable,
error messages for LaTeXML.  Its routines are used by the error reporting methods
from L<LaTeXML::Global>, namely C<Warn>, C<Error> and C<Fatal>.

The general idea is that a minimal amount should be printed to STDERR (possibly with
colors, spinners, etc if it is a terminal), and more complete information is printed to
a log file. Neither of these are enabled, by default; see below.

=over 4

=item C<< SetVerbosity($verbosity); >>

Controls the verbosity of output to the terminal;
default is 0, higher gives more information, lower gives less.
A verbosity less than 0 inhibits all output to STDERR.

=item C<< UseSTDERR(); ... UseSTDERR(undef); >>

C<< UseSTDERR(); >> Enables and initializes STDERR to accept messages.
If this is not called, there will be no output to STDERR.
C<< UseSTDERR(undef); >> disables STDERR from further messages.

=item C<< UseLog($path, $append); ... UseLog(undef); >>

C<< UseLog($path, $append); >> opens a log file on the given path.
If C<$append> is true, this file will be appended to,
otherwise, it will be created initially empty.
If this is not called, there will be no log file.
C<< UseLog(undef); >> disables and closes the log file.

=back

=head2 Error Reporting

The Error reporting functions all take a similar set of arguments,
the differences are in the implied severity of the situation,
and in the amount of detail that will be reported.

The C<$category> is a string naming a broad category of errors,
such as "undefined". The set is open-ended, but see the manual
for a list of recognized categories.  C<$object> is the object
whose presence or lack caused the problem.

C<$where> indicates where the problem occurred; passs in
the C<$gullet> or C<$stomach> if the problem occurred during
expansion or digestion; pass in a document node if it occurred there.
A string will be used as is; if an undefined value is used,
the error handler will try to guess.

The C<$message> should be a somewhat concise, but readable,
explanation of the problem, but ought to not refer to the
document or any "incident specific" information, so as to
support indexing in build systems.  C<@details> provides
additional lines of information that may be indident specific.

=over 4

=item C<< Fatal($category,$object,$where,$message,@details); >>

Signals an fatal error, printing C<$message> along with some context.
In verbose mode a stack trace is printed.

=item C<< Error($category,$object,$where,$message,@details); >>

Signals an error, printing C<$message> along with some context.
If in strict mode, this is the same as Fatal().
Otherwise, it attempts to continue processing..

=item C<< Warn($category,$object,$where,$message,@details); >>

Prints a warning message along with a short indicator of
the input context, unless verbosity is quiet.

=item C<< Info($category,$object,$where,$message,@details); >>

Prints an informational message along with a short indicator of
the input context, unless verbosity is quiet.

=back

=head2 Progress Reporting

=over 4

=item C<< Note($message); >>

General status message, printed whenever verbosity at or above 0,
to both STDERR and the Log file (when enabled).

=item C<< NoteLog($message); >>

Prints a status message to the Log file (when enabled).

=item C<< NoteSTDERR($message); >>

Prints a status message to the terminal (STDERR) (when enabled).

=item C<< ProgressSpinup($stage); >>

Begin a processing stage, which will be ended with C<ProgressSpindown($stage)>;
This prints a message to the log such as "(stage... runtime)", where runtime is the time required.
In conjunction with C<ProgressStep()>, creates a progress spinner on STDERR.

=item C<< ProgressSpinup($stage); >>

End a processing stage bugin with C<ProgressSpindown($stage);>.

=item C<< ProgressStep(); >>

Steps a progress spinner on STDERR.

=back

=head2 Debugging

Debugging statements may be embedded throughout the program. These are associated with a
feature keyword.  A given feature is enabled using the command-line option
C<--debug=feature>.

=over 4

=item C<< Debug($message) if $LaTeXML::DEBUG{$feature} >>

Prints C<$message> if debugging has been enabled for the given feature.

=item C<< DebuggableFeature($feature,$description) >>

Declare that C<$feature> is a known debuggable feature, and give a description of it.

=item C<< CheckDebuggable() >>

A untility to check and report if all requested debugging features actually have debugging messages
declared.

=back

=head2 Internal Functions

No user serviceable parts inside.  These symbols are not exported.

=over 4

=item C<< $string = LaTeXML::Common::Error::generateMessage($typ,$msg,$lng,@more); >>

Constructs an error or warning message based on the current stack and
the current location in the document.
C<$typ> is a short string characterizing the type of message, such as "Error".
C<$msg> is the error message itself. If C<$lng> is true, will generate a
more verbose message; this also uses the VERBOSITY set in the C<$STATE>.
Longer messages will show a trace of the objects invoked on the stack,
C<@more> are additional strings to include in the message.

=item C<< $string = LaTeXML::Common::Error::stacktrace; >>

Return a formatted string showing a trace of the stackframes up until this
function was invoked.

=item C<< @objects = LaTeXML::Common::Error::objectStack; >>

Return a list of objects invoked on the stack.  This procedure only
considers those stackframes which involve methods, and the objects are
those (unique) objects that the method was called on.

=back

=head1 AUTHOR

Bruce Miller <bruce.miller@nist.gov>

=head1 COPYRIGHT

Public domain software, produced as part of work done by the
United States Government & not subject to copyright in the US.

=cut