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
|