// // Fl_Help_View widget for the Fast Light Tool Kit (FLTK). // // Copyright 1997-2010 by Easy Software Products. // Image support by Matthias Melcher, Copyright 2000-2009. // // Buffer management (HV_Edit_Buffer) and more by AlbrechtS and others. // Copyright 2011-2025 by Bill Spitzak and others. // // This library is free software. Distribution and use rights are outlined in // the file "COPYING" which should have been included with this file. If this // file is missing or damaged, see the license at: // // https://www.fltk.org/COPYING.php // // Please see the following page on how to report bugs and issues: // // https://www.fltk.org/bugs.php // // // Include necessary header files... // #include #include #include #include #include #include "Fl_Int_Vector.H" #include #include #include #include // fl_open_uri() #include // fl_strdup() #include "flstring.h" #include #include #include #include #define MAX_COLUMNS 200 // // Typedef the C API sort function type the only way I know how... // extern "C" { typedef int (*compare_func_t)(const void *, const void *); } // // Local functions... // static int quote_char(const char *); static void scrollbar_callback(Fl_Widget *s, void *); static void hscrollbar_callback(Fl_Widget *s, void *); static std::string to_lower(const std::string &str) { std::string lower_str; lower_str.reserve(str.size()); for (char c : str) { lower_str += fl_tolower(c); } return lower_str; } // This function skips 'n' bytes *within* a string, i.e. it checks // for a NUL byte as string terminator. // If a NUL byte is found before 'n' bytes have been scanned it returns // a pointer to the end of the string (the NUL byte). // This avoids pure pointer arithmetic that would potentially overrun // the end of the string (buffer), see GitHub Issue #1196. // // Note: this should be rewritten and improved in FLTK 1.5.0 or later // by using std::string etc.. static char *skip_bytes(const char *p, int n) { for (int i = 0; i < n; ++i) { if (*p == '\0') break; ++p; } return const_cast(p); } // // global flag for image loading (see get_image). // static char initial_load = 0; // // Broken image... // static const char * const broken_xpm[] = { "16 24 4 1", "@ c #000000", " c #ffffff", "+ c none", "x c #ff0000", // pixels "@@@@@@@+++++++++", "@ @++++++++++", "@ @+++++++++++", "@ @++@++++++++", "@ @@+++++++++", "@ @+++@+++++", "@ @++@@++++@", "@ xxx @@ @++@@", "@ xxx xx@@ @", "@ xxx xxx @", "@ xxxxxx @", "@ xxxx @", "@ xxxxxx @", "@ xxx xxx @", "@ xxx xxx @", "@ xxx xxx @", "@ @", "@ @", "@ @", "@ @", "@ @", "@ @", "@ @", "@@@@@@@@@@@@@@@@", NULL }; static Fl_Pixmap broken_image(broken_xpm); /** This text may be customized at run-time. */ const char *Fl_Help_View::copy_menu_text = "Copy"; static Fl_Menu_Item rmb_menu[] = { { NULL, 0, NULL, (void*)1 }, // Copy { NULL } }; // // Simple margin stack for Fl_Help_View::format()... // class Margin_Stack { std::vector margins_; public: Margin_Stack() = default; void clear() { margins_.clear(); margins_.push_back(4); // default margin } int current() { return margins_.back(); } int pop() { if (margins_.size() > 1) { margins_.pop_back(); } return margins_.back(); } int push(int indent) { int xx = current() + indent; margins_.push_back(xx); return xx; } }; /** \brief Constructs a Font_Style object with the specified font, size, and color. \param afont The font to use. \param asize The font size. \param acolor The font color. */ Fl_Help_View::Font_Style::Font_Style(Fl_Font afont, Fl_Fontsize asize, Fl_Color acolor) { set(afont, asize, acolor); } /** \brief Retrieves the font, font size, and color settings from this Font_Style instance. \param[out] afont Reference to a variable where the font will be stored. \param[out] asize Reference to a variable where the font size will be stored. \param[out] acolor Reference to a variable where the font color will be stored. */ void Fl_Help_View::Font_Style::get(Fl_Font &afont, Fl_Fontsize &asize, Fl_Color &acolor) { afont=f; asize=s; acolor=c; } /** \brief Sets the font, font size, and color for the Font_Style. Thi only set the memebrs of the class, but does not change the current rendering settings. \param afont The font to be used. \param asize The font size to be set. \param acolor The color to be applied to the font. */ void Fl_Help_View::Font_Style::set(Fl_Font afont, Fl_Fontsize asize, Fl_Color acolor) { f=afont; s=asize; c=acolor; } /** \brief Initializes the font stack with a default font, size, and color. Clears the stack and pushes one element with a default font, size, and color. \param[in] f font to apply \param[in] s font size to apply \param[in] c color to apply */ void Fl_Help_View::Font_Stack::init(Fl_Font f, Fl_Fontsize s, Fl_Color c) { elts_.clear(); push(f, s, c); } /** \brief Gets the top (current) element on the stack. \param[out] f font to apply \param[out] s font size to apply \param[out] c color to apply \note This function does not pop the stack, it just returns the top element. */ void Fl_Help_View::Font_Stack::top(Fl_Font &f, Fl_Fontsize &s, Fl_Color &c) { elts_.back().get(f, s, c); } /** \brief Push the font style triplet on the stack. Also calls fl_font() and fl_color() adequately \param[in] f font to apply \param[in] s font size to apply \param[in] c color to apply */ void Fl_Help_View::Font_Stack::push(Fl_Font f, Fl_Fontsize s, Fl_Color c) { elts_.push_back(Font_Style(f, s, c)); fl_font(f, s); fl_color(c); } /** \brief Pop style form the stack and apply new top style. Pops from the stack the font style triplet and calls fl_font() and fl_color() adequately \param[out] f font to apply \param[out] s font size to apply \param[out] c color to apply \note If the stack has only one element left, that element will not be popped, but the top element will be applied again. */ void Fl_Help_View::Font_Stack::pop(Fl_Font &f, Fl_Fontsize &s, Fl_Color &c) { if (elts_.size() > 1) elts_.pop_back(); top(f, s, c); fl_font(f, s); fl_color(c); } /** \brief Gets the current count of font style elements in the stack. \return stack size in number of elements */ size_t Fl_Help_View::Font_Stack::count() const { return elts_.size(); } // // All the stuff needed to implement text selection in Fl_Help_View // /* matt: The selection code was implemented with binary compatibility within 1.4 in mind. This means that I was limited to adding static variables only to not enlarge the Fl_Help_View class. The implementation of selection should be change to use class local variables so text in multiple Fl_Help_View widgets can be selected independently. Still to do: - &word; style characters mess up our count inside a word boundary - we can only select words, no individual characters - no dragging of the selection into another widget - selection must be cleared if another widget get focus! - write a comment for every new function */ /* The following functions are also used to draw stuff and should be replaced with local copies that are much faster when merely counting: fl_color(Fl_Color); fl_rectf(int, int, int, int); fl_push_clip(int, int, int, int); fl_xyline(int, int, int); fl_rect() fl_line() img->draw() */ // We don't put the offscreen buffer in the help view class because // we'd need to include platform.H in the header... static Fl_Offscreen fl_help_view_buffer; int Fl_Help_View::selection_first_ = 0; int Fl_Help_View::selection_last_ = 0; int Fl_Help_View::selection_push_first_ = 0; int Fl_Help_View::selection_push_last_ = 0; int Fl_Help_View::selection_drag_first_ = 0; int Fl_Help_View::selection_drag_last_ = 0; int Fl_Help_View::selected_ = 0; int Fl_Help_View::draw_mode_ = 0; int Fl_Help_View::mouse_x_ = 0; int Fl_Help_View::mouse_y_ = 0; int Fl_Help_View::current_pos_ = 0; Fl_Help_View *Fl_Help_View::current_view_ = 0L; Fl_Color Fl_Help_View::hv_selection_color_; Fl_Color Fl_Help_View::hv_selection_text_color_; /** \brief Draws a text string in the help view. This function draws the text string \p t at position (\p x, \p y) in the help view. If the text is selected, it draws a selection rectangle around it and changes the text color. \param[in] t Text to draw \param[in] x X position to draw at \param[in] y Y position to draw at \param[in] entity_extra_length (unclear) */ void Fl_Help_View::hv_draw(const char *t, int x, int y, int entity_extra_length) { if (selected_ && current_view_==this && current_pos_=selection_first_) { Fl_Color c = fl_color(); fl_color(hv_selection_color_); int w = (int)fl_width(t); if (current_pos_+(int)strlen(t)=x && mouse_x_=y-fl_height()+fl_descent()&&mouse_y_<=y+fl_descent()) { int f = (int) current_pos_; int l = (int) (f+strlen(t)); // use 'quote_char' to calculate the true length of the HTML string if (draw_mode_==1) { selection_push_first_ = f; selection_push_last_ = l; } else { selection_drag_first_ = f; selection_drag_last_ = l + entity_extra_length; } } } } } // [Internal class HV_Edit_Buffer] // Debug: set to 1 for basic debugging, 2 for more, 0 for none #define DEBUG_EDIT_BUFFER 0 #if (DEBUG_EDIT_BUFFER > 1) #define DEBUG_FUNCTION(L,F) \ printf("\n========\n [%d] --- %s\n========\n", L, F); \ fflush(stdout); #else #define DEBUG_FUNCTION(L,F) #endif /* Note: Don't use Doxygen docs for this internal class. Internal class to manage the Fl_Help_View edit buffer. This is a subclass of Fl_String since FLTK 1.4.0 and std::string since 1.5.0. This class is for internal use in this file. Its sole purpose is to allow buffer management to avoid buffer overflows in stack variables used to edit strings for formatting and drawing (STR #3275). */ class HV_Edit_Buffer : public std::string { public: // use default constructor and destructor, none defined here // append a Unicode character (Code Point) to the string void add(int ucs); // case insensitive comparison of buffer contents with a string int cmp(const char *str) { return !strcasecmp(c_str(), str); } // string width of the entire buffer contents int width() { return (int)fl_width(c_str()); } #if (DEBUG_EDIT_BUFFER) void print(const char *text = ""); #endif }; /* Append one Unicode character (code point) to the buffer. The Unicode character \p ucs is converted to UTF-8 and appended to the buffer. \param[in] ucs Unicode character (code point) to be added */ void HV_Edit_Buffer::add(int ucs) { int len; char cbuf[6]; len = fl_utf8encode((unsigned int)ucs, cbuf); if (len < 1) len = 1; append(cbuf, len); } // add(int ucs) /* Print the edit buffer (Debug only). */ #if (DEBUG_EDIT_BUFFER) void HV_Edit_Buffer::print(const char *text) { printf("HV_Edit_Buffer::print(%s), capacity=%d, size=%d\n", text, capacity(), size()); printf(" \"%s\"\n", c_str() && size() ? c_str() : ""); fflush(stdout); } // print() #endif // [End of internal class HV_Edit_Buffer] /** \brief Adds a text block to the list. \param[in] s Pointer to start of block text \param[in] xx X position of block \param[in] yy Y position of block \param[in] ww Right margin of block \param[in] hh Height of block \param[in] border Draw border? \return Pointer to the new block in the list. */ Fl_Help_View::Text_Block *Fl_Help_View::add_block( const char *s, int xx, int yy, int ww, int hh, unsigned char border) { Text_Block *temp; // New block // printf("add_block(s = %p, xx = %d, yy = %d, ww = %d, hh = %d, border = %d)\n", // s, xx, yy, ww, hh, border); blocks_.push_back(Text_Block()); // Add a new block to the vector temp = &blocks_.back(); memset(temp, 0, sizeof(Text_Block)); temp->start = s; temp->end = s; temp->x = xx; temp->y = yy; temp->w = ww; temp->h = hh; temp->border = border; temp->bgcolor = bgcolor_; return (temp); } /** \brief Add a new link and its postion on screen to the link list. \param[in] link a filename, followed by a hash and a target. All parts are optional. \param[in] xx, yy, ww, hh bounding box of the link text on screen */ void Fl_Help_View::add_link(const std::string &link, int xx, int yy, int ww, int hh) { auto new_link = std::make_shared(); // Create a new link storage object. new_link->box = { xx, yy, ww, hh }; size_t hash_pos = link.find('#'); // Find the hash character if (hash_pos != std::string::npos) { // If a '#' is found, split the link into filename and target new_link->filename_ = link.substr(0, hash_pos); new_link->target = link.substr(hash_pos + 1); } else { // No '#' found, use the whole link as filename new_link->filename_ = link; new_link->target.clear(); } link_list_.push_back(new_link); // Add the link to the list. } /** \brief Adds a new target to the list. \param[in] n Name of target (string) \param[in] yy line number of traget position */ void Fl_Help_View::add_target(const std::string &n, int yy) { std::string target = to_lower(n); // Convert target name to lower case target_line_map_[target] = yy; // Store the target line in the map } /** \brief Computes the alignment for a line in a block. \param[in] block Pointer to the block to add to \param[in] line Current line number in the block \param[in] xx Current X position in the block \param[in] a Current alignment \param[in,out] l Starting link index for alignment adjustment \return The new line number after alignment adjustment */ int Fl_Help_View::do_align( Text_Block *block, int line, int xx, int a, int &l) { int offset; // Alignment offset switch (a) { case RIGHT : // Right align offset = block->w - xx; break; case CENTER : // Center offset = (block->w - xx) / 2; break; default : // Left align offset = 0; break; } block->line[line] = block->x + offset; if (line < 31) line ++; while (l < (int)link_list_.size()) { link_list_[l]->box.x( link_list_[l]->box.x() + offset); l++; } return (line); } /** \brief Draws the Fl_Help_View widget. */ void Fl_Help_View::draw() { int i; // Looping var const Text_Block *block; // Pointer to current block const char *ptr, // Pointer to text in block *attrs; // Pointer to start of element attributes HV_Edit_Buffer buf; // Text buffer char attr[1024]; // Attribute buffer int xx, yy, ww, hh; // Current positions and sizes int line; // Current line Fl_Font font; Fl_Fontsize fsize; // Current font and size Fl_Color fcolor; // current font color int head, pre, // Flags for text needspace; // Do we need whitespace? Fl_Boxtype b = box() ? box() : FL_DOWN_BOX; // Box to draw... int underline, // Underline text? xtra_ww; // Extra width for underlined space between words DEBUG_FUNCTION(__LINE__,__FUNCTION__); // Draw the scrollbar(s) and box first... ww = w(); hh = h(); i = 0; draw_box(b, x(), y(), ww, hh, bgcolor_); if ( hscrollbar_.visible() || scrollbar_.visible() ) { int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size(); int hor_vis = hscrollbar_.visible(); int ver_vis = scrollbar_.visible(); // Scrollbar corner int scorn_x = x() + ww - (ver_vis?scrollsize:0) - Fl::box_dw(b) + Fl::box_dx(b); int scorn_y = y() + hh - (hor_vis?scrollsize:0) - Fl::box_dh(b) + Fl::box_dy(b); if ( hor_vis ) { if ( hscrollbar_.h() != scrollsize ) { // scrollsize changed? hscrollbar_.resize(x(), scorn_y, scorn_x - x(), scrollsize); init_sizes(); } draw_child(hscrollbar_); hh -= scrollsize; } if ( ver_vis ) { if ( scrollbar_.w() != scrollsize ) { // scrollsize changed? scrollbar_.resize(scorn_x, y(), scrollsize, scorn_y - y()); init_sizes(); } draw_child(scrollbar_); ww -= scrollsize; } if ( hor_vis && ver_vis ) { // Both scrollbars visible? Draw little gray box in corner fl_color(FL_GRAY); fl_rectf(scorn_x, scorn_y, scrollsize, scrollsize); } } if (!value_) return; if (current_view_ == this && selected_) { hv_selection_color_ = FL_SELECTION_COLOR; hv_selection_text_color_ = fl_contrast(textcolor_, FL_SELECTION_COLOR); } current_pos_ = 0; // Clip the drawing to the inside of the box... fl_push_clip(x() + Fl::box_dx(b), y() + Fl::box_dy(b), ww - Fl::box_dw(b), hh - Fl::box_dh(b)); fl_color(textcolor_); // Draw all visible blocks... for (i = 0, block = &blocks_[0]; i < (int)blocks_.size(); i ++, block ++) if ((block->y + block->h) >= topline_ && block->y < (topline_ + h())) { line = 0; xx = block->line[line]; yy = block->y - topline_; hh = 0; pre = 0; head = 0; needspace = 0; underline = 0; initfont(font, fsize, fcolor); // byte length difference between html entity (encoded by &...;) and // UTF-8 encoding of same character int entity_extra_length = 0; for (ptr = block->start, buf.clear(); ptr < block->end;) { if ((*ptr == '<' || isspace((*ptr)&255)) && buf.size() > 0) { if (!head && !pre) { // Check width... ww = buf.width(); if (needspace && xx > block->x) xx += (int)fl_width(' '); if ((xx + ww) > block->w) { if (line < 31) line ++; xx = block->line[line]; yy += hh; hh = 0; } hv_draw(buf.c_str(), xx + x() - leftline_, yy + y(), entity_extra_length); buf.clear(); entity_extra_length = 0; if (underline) { xtra_ww = isspace((*ptr)&255)?(int)fl_width(' '):0; fl_xyline(xx + x() - leftline_, yy + y() + 1, xx + x() - leftline_ + ww + xtra_ww); } current_pos_ = (int) (ptr-value_); xx += ww; if ((fsize + 2) > hh) hh = fsize + 2; needspace = 0; } else if (pre) { while (isspace((*ptr)&255)) { if (*ptr == '\n') { hv_draw(buf.c_str(), xx + x() - leftline_, yy + y()); if (underline) fl_xyline(xx + x() - leftline_, yy + y() + 1, xx + x() - leftline_ + buf.width()); buf.clear(); current_pos_ = (int) (ptr-value_); if (line < 31) line ++; xx = block->line[line]; yy += hh; hh = fsize + 2; } else if (*ptr == '\t') { // Do tabs every 8 columns... buf += ' '; // add at least one space while (buf.size() & 7) buf += ' '; } else { buf += ' '; } if ((fsize + 2) > hh) hh = fsize + 2; ptr ++; } if (buf.size() > 0) { hv_draw(buf.c_str(), xx + x() - leftline_, yy + y()); ww = buf.width(); buf.clear(); if (underline) fl_xyline(xx + x() - leftline_, yy + y() + 1, xx + x() - leftline_ + ww); xx += ww; current_pos_ = (int) (ptr-value_); } needspace = 0; } else { buf.clear(); while (isspace((*ptr)&255)) ptr ++; current_pos_ = (int) (ptr-value_); } } if (*ptr == '<') { ptr ++; if (strncmp(ptr, "!--", 3) == 0) { // Comment... ptr += 3; if ((ptr = strstr(ptr, "-->")) != NULL) { ptr += 3; continue; } else break; } while (*ptr && *ptr != '>' && !isspace((*ptr)&255)) buf += *ptr++; attrs = ptr; while (*ptr && *ptr != '>') ptr ++; if (*ptr == '>') ptr ++; // end of command reached, set the supposed start of printed eord here current_pos_ = (int) (ptr-value_); if (buf.cmp("HEAD")) head = 1; else if (buf.cmp("BR")) { if (line < 31) line ++; xx = block->line[line]; yy += hh; hh = 0; } else if (buf.cmp("HR")) { fl_line(block->x + x(), yy + y(), block->w + x(), yy + y()); if (line < 31) line ++; xx = block->line[line]; yy += 2 * fsize;//hh; hh = 0; } else if (buf.cmp("CENTER") || buf.cmp("P") || buf.cmp("H1") || buf.cmp("H2") || buf.cmp("H3") || buf.cmp("H4") || buf.cmp("H5") || buf.cmp("H6") || buf.cmp("UL") || buf.cmp("OL") || buf.cmp("DL") || buf.cmp("LI") || buf.cmp("DD") || buf.cmp("DT") || buf.cmp("PRE")) { if (tolower(buf[0]) == 'h') { font = FL_HELVETICA_BOLD; fsize = textsize_ + '7' - buf[1]; } else if (buf.cmp("DT")) { font = textfont_ | FL_ITALIC; fsize = textsize_; } else if (buf.cmp("PRE")) { font = FL_COURIER; fsize = textsize_; pre = 1; } if (buf.cmp("LI")) { if (block->ol) { char buf[10]; snprintf(buf, sizeof(buf), "%d. ", block->ol_num); hv_draw(buf, xx - (int)fl_width(buf) + x() - leftline_, yy + y()); } else { // draw bullet (•) Unicode: U+2022, UTF-8 (hex): e2 80 a2 unsigned char bullet[4] = { 0xe2, 0x80, 0xa2, 0x00 }; hv_draw((char *)bullet, xx - fsize + x() - leftline_, yy + y()); } } pushfont(font, fsize); buf.clear(); } else if (buf.cmp("A") && get_attr(attrs, "HREF", attr, sizeof(attr)) != NULL) { fl_color(linkcolor_); underline = 1; } else if (buf.cmp("/A")) { fl_color(textcolor_); underline = 0; } else if (buf.cmp("FONT")) { if (get_attr(attrs, "COLOR", attr, sizeof(attr)) != NULL) { textcolor_ = get_color(attr, textcolor_); } if (get_attr(attrs, "FACE", attr, sizeof(attr)) != NULL) { if (!strncasecmp(attr, "helvetica", 9) || !strncasecmp(attr, "arial", 5) || !strncasecmp(attr, "sans", 4)) font = FL_HELVETICA; else if (!strncasecmp(attr, "times", 5) || !strncasecmp(attr, "serif", 5)) font = FL_TIMES; else if (!strncasecmp(attr, "symbol", 6)) font = FL_SYMBOL; else font = FL_COURIER; } if (get_attr(attrs, "SIZE", attr, sizeof(attr)) != NULL) { if (isdigit(attr[0] & 255)) { // Absolute size fsize = (int)(textsize_ * pow(1.2, atof(attr) - 3.0)); } else { // Relative size fsize = (int)(fsize * pow(1.2, atof(attr) - 3.0)); } } pushfont(font, fsize); } else if (buf.cmp("/FONT")) { popfont(font, fsize, textcolor_); } else if (buf.cmp("U")) underline = 1; else if (buf.cmp("/U")) underline = 0; else if (buf.cmp("B") || buf.cmp("STRONG")) pushfont(font |= FL_BOLD, fsize); else if (buf.cmp("TD") || buf.cmp("TH")) { int tx, ty, tw, th; if (tolower(buf[1]) == 'h') pushfont(font |= FL_BOLD, fsize); else pushfont(font = textfont_, fsize); tx = block->x - 4 - leftline_; ty = block->y - topline_ - fsize - 3; tw = block->w - block->x + 7; th = block->h + fsize - 5; if (tx < 0) { tw += tx; tx = 0; } if (ty < 0) { th += ty; ty = 0; } tx += x(); ty += y(); if (block->bgcolor != bgcolor_) { fl_color(block->bgcolor); fl_rectf(tx, ty, tw, th); fl_color(textcolor_); } if (block->border) fl_rect(tx, ty, tw, th); } else if (buf.cmp("I") || buf.cmp("EM")) pushfont(font |= FL_ITALIC, fsize); else if (buf.cmp("CODE") || buf.cmp("TT")) pushfont(font = FL_COURIER, fsize); else if (buf.cmp("KBD")) pushfont(font = FL_COURIER_BOLD, fsize); else if (buf.cmp("VAR")) pushfont(font = FL_COURIER_ITALIC, fsize); else if (buf.cmp("/HEAD")) head = 0; else if (buf.cmp("/H1") || buf.cmp("/H2") || buf.cmp("/H3") || buf.cmp("/H4") || buf.cmp("/H5") || buf.cmp("/H6") || buf.cmp("/B") || buf.cmp("/STRONG") || buf.cmp("/I") || buf.cmp("/EM") || buf.cmp("/CODE") || buf.cmp("/TT") || buf.cmp("/KBD") || buf.cmp("/VAR")) popfont(font, fsize, fcolor); else if (buf.cmp("/PRE")) { popfont(font, fsize, fcolor); pre = 0; } else if (buf.cmp("IMG")) { Fl_Shared_Image *img = 0; int width, height; char wattr[8], hattr[8]; get_attr(attrs, "WIDTH", wattr, sizeof(wattr)); get_attr(attrs, "HEIGHT", hattr, sizeof(hattr)); width = get_length(wattr); height = get_length(hattr); if (get_attr(attrs, "SRC", attr, sizeof(attr))) { img = get_image(attr, width, height); if (!width) width = img->w(); if (!height) height = img->h(); } if (!width || !height) { if (get_attr(attrs, "ALT", attr, sizeof(attr)) == NULL) { strcpy(attr, "IMG"); } } ww = width; if (needspace && xx > block->x) xx += (int)fl_width(' '); if ((xx + ww) > block->w) { if (line < 31) line ++; xx = block->line[line]; yy += hh; hh = 0; } if (img) { img->draw(xx + x() - leftline_, yy + y() - fl_height() + fl_descent() + 2); } xx += ww; if ((height + 2) > hh) hh = height + 2; needspace = 0; } buf.clear(); } else if (*ptr == '\n' && pre) { hv_draw(buf.c_str(), xx + x() - leftline_, yy + y()); buf.clear(); if (line < 31) line ++; xx = block->line[line]; yy += hh; hh = fsize + 2; needspace = 0; ptr ++; current_pos_ = (int) (ptr-value_); } else if (isspace((*ptr)&255)) { if (pre) { if (*ptr == ' ') buf += ' '; else { // Do tabs every 8 columns... buf += ' '; // at least one space while (buf.size() & 7) buf += ' '; } } ptr ++; if (!pre) current_pos_ = (int) (ptr-value_); needspace = 1; } else if (*ptr == '&') // process html entity { ptr ++; int qch = quote_char(ptr); if (qch < 0) buf += '&'; else { size_t utf8l = buf.size(); buf.add(qch); utf8l = buf.size() - utf8l; // length of added UTF-8 text const char *oldptr = ptr; ptr = strchr(ptr, ';') + 1; entity_extra_length += int(ptr - (oldptr-1)) - utf8l; // extra length between html entity and UTF-8 } if ((fsize + 2) > hh) hh = fsize + 2; } else { buf += *ptr++; if ((fsize + 2) > hh) hh = fsize + 2; } } if (buf.size() > 0 && !pre && !head) { ww = buf.width(); if (needspace && xx > block->x) xx += (int)fl_width(' '); if ((xx + ww) > block->w) { if (line < 31) line ++; xx = block->line[line]; yy += hh; hh = 0; } } if (buf.size() > 0 && !head) { hv_draw(buf.c_str(), xx + x() - leftline_, yy + y()); if (underline) fl_xyline(xx + x() - leftline_, yy + y() + 1, xx + x() - leftline_ + ww); current_pos_ = (int) (ptr-value_); } } fl_pop_clip(); } // draw() /** \brief Skips over HTML tags in a text. In an html style text, set the character pointer p, skipping anything from a leading '<' up to and including the closing '>'. If the end of the buffer is reached, the function returns `end`. No need to handle UTF-8 here. \param[in] p pointer to html text, UTF-8 characters possible \param[in] end pointer to the end of the text (need nut be NUL) \return new pointer to text after skipping over '<...>' blocks, or `end` if NUL was found or a '<...>' block was not closed. */ static const char *vanilla(const char *p, const char *end) { if (*p == '\0' || p >= end) return end; for (;;) { if (*p != '<') { return p; } else { while (*p && p < end && *p != '>') p++; } p++; if (*p == '\0' || p >= end) return end; } } /** \brief Finds the specified string \p s at starting position \p p. The argument \p p and the return value are offsets in Fl_Help_View::value(), counting from 0. If \p p is out of range, 0 is used. The string comparison is simple but honors some special cases: - the specified string \p s must be in UTF-8 encoding - HTML tags in value() are filtered (not compared as such, they never match) - HTML entities like '\<' or '\&x#20ac;' are converted to Unicode (UTF-8) - ASCII characters (7-bit, \< 0x80) are compared case insensitive - every newline (LF, '\\n') in value() is treated like a single space - all other strings are compared as-is (byte by byte) \param[in] s search string in UTF-8 encoding \param[in] p starting position for search (0,...), Default = 0 \return the matching position or -1 if not found */ int Fl_Help_View::find(const char *s, int p) { int i, // Looping var c; // Current character Text_Block *b; // Current block const char *bp, // Block matching pointer *bs, // Start of current comparison *sp; // Search string pointer DEBUG_FUNCTION(__LINE__,__FUNCTION__); // Range check input and value... if (!s || !value_) return -1; if (p < 0 || p >= (int)strlen(value_)) p = 0; // Look for the string... for (i = (int)blocks_.size(), b = &blocks_[0]; i > 0; i--, b++) { if (b->end < (value_ + p)) continue; if (b->start < (value_ + p)) bp = value_ + p; else bp = b->start; bp = vanilla(bp, b->end); if (bp == b->end) continue; for (sp = s, bs = bp; *sp && *bp && bp < b->end; ) { bool is_html_entity = false; if (*bp == '&') { // decode HTML entity... if ((c = quote_char(bp + 1)) < 0) { c = '&'; } else { const char *entity_end = strchr(bp + 1, ';'); if (entity_end) { is_html_entity = true; // c contains the unicode character bp = entity_end; } else { c = '&'; } } } else { c = *bp; } if (c == '\n') c = ' '; // treat newline as a single space // *FIXME* *UTF-8* (A.S. 02/14/2016) // At this point c may be an arbitrary Unicode Code Point corresponding // to a quoted character (see above), i.e. it _can_ be a multi byte // UTF-8 sequence and must be compared with the corresponding // multi byte string in (*sp)... // For instance: "€" == 0x20ac -> 0xe2 0x82 0xac (UTF-8: 3 bytes). // Hint: use fl_utf8encode() [see below] int utf_len = 1; if (c > 0x20 && c < 0x80 && tolower(*sp) == tolower(c)) { // Check for ASCII case insensitive match. //printf("%ld text match %c/%c\n", bp-value_, *sp, c); sp++; bp = vanilla(bp+1, b->end); } else if (is_html_entity && fl_utf8decode(sp, NULL, &utf_len) == (unsigned int)c ) { // Check if a < entity ini html matches a UTF-8 character in the // search string. //printf("%ld unicode match 0x%02X 0x%02X\n", bp-value_, *sp, c); sp += utf_len; bp = vanilla(bp+1, b->end); } else if (*sp == c) { // Check if UTF-8 bytes in html and the search string match. //printf("%ld binary match %c/%c\n", bp-value_, *sp, c); sp++; bp = vanilla(bp+1, b->end); } else { // No match, so reset to start of search... . //printf("reset search (%c/%c)\n", *sp, c); sp = s; bp = bs = vanilla(bs+1, b->end); } } if (!*sp) { // Found a match! topline(b->y - b->h); return int(bs - value_); } } // No match! return (-1); } /** \brief Formats the help text. */ void Fl_Help_View::format() { int i; // Looping var int done; // Are we done yet? Text_Block *block, // Current block *cell; // Current table cell int cells[MAX_COLUMNS], // Cells in the current row... row; // Current table row (block number) const char *ptr, // Pointer into block *start, // Pointer to start of element *attrs; // Pointer to start of element attributes HV_Edit_Buffer buf; // Text buffer char attr[1024], // Attribute buffer wattr[1024], // Width attribute buffer hattr[1024], // Height attribute buffer linkdest[1024]; // Link destination int xx, yy, ww, hh; // Size of current text fragment int line; // Current line in block int links; // Links for current line Fl_Font font; Fl_Fontsize fsize; // Current font and size Fl_Color fcolor; // Current font color unsigned char border; // Draw border? int talign, // Current alignment newalign, // New alignment head, // In the section? pre, //
 text?
                needspace;      // Do we need whitespace?
  int           table_width,    // Width of table
                table_offset;   // Offset of table
  int           column,         // Current table column number
                columns[MAX_COLUMNS];
                                // Column widths
  Fl_Color      tc, rc;         // Table/row background color
  Fl_Boxtype    b = box() ? box() : FL_DOWN_BOX;
                                // Box to draw...
  Margin_Stack  margins;        // Left margin stack...
  Fl_Int_Vector OL_num;         // if nonnegative, in OL mode and this is the item number

  OL_num.push_back(-1);

  DEBUG_FUNCTION(__LINE__,__FUNCTION__);

  // Reset document width...
  int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
  hsize_ = w() - scrollsize - Fl::box_dw(b);

  done = 0;
  while (!done)
  {
    // Reset state variables...
    done       = 1;
    blocks_.clear();
    link_list_.clear();
    target_line_map_.clear();
    size_      = 0;
    bgcolor_   = color();
    textcolor_ = textcolor();
    linkcolor_ = fl_contrast(FL_BLUE, color());

    tc = rc = bgcolor_;

    title_ = "Untitled";

    if (!value_)
      return;

    // Setup for formatting...
    initfont(font, fsize, fcolor);

    line         = 0;
    links        = 0;
    margins.clear();
    xx           = 4;
    yy           = fsize + 2;
    ww           = 0;
    column       = 0;
    border       = 0;
    hh           = 0;
    block        = add_block(value_, xx, yy, hsize_, 0);
    row          = 0;
    head         = 0;
    pre          = 0;
    talign       = LEFT;
    newalign     = LEFT;
    needspace    = 0;
    linkdest[0]  = '\0';
    table_offset = 0;

    // Html text character loop
    for (ptr = value_, buf.clear(); *ptr;)
    {
      // End of word?
      if ((*ptr == '<' || isspace((*ptr)&255)) && buf.size() > 0)
      {
        // Get width of word parsed so far...
        ww = buf.width();

        if (!head && !pre)
        {
          // Check width...
          if (ww > hsize_) {
            hsize_ = ww;
            done   = 0;
            break;
          }

          if (needspace && xx > block->x)
            ww += (int)fl_width(' ');

  //        printf("line = %d, xx = %d, ww = %d, block->x = %d, block->w = %d\n",
  //           line, xx, ww, block->x, block->w);

          if ((xx + ww) > block->w)
          {
            line     = do_align(block, line, xx, newalign, links);
            xx       = block->x;
            yy       += hh;
            block->h += hh;
            hh       = 0;
          }

          if (linkdest[0])
            add_link(linkdest, xx, yy - fsize, ww, fsize);

          xx += ww;
          if ((fsize + 2) > hh)
            hh = fsize + 2;

          needspace = 0;
        }
        else if (pre)
        {
          // Add a link as needed...
          if (linkdest[0])
            add_link(linkdest, xx, yy - hh, ww, hh);

          xx += ww;
          if ((fsize + 2) > hh)
            hh = fsize + 2;

          // Handle preformatted text...
          while (isspace((*ptr)&255))
          {
            if (*ptr == '\n')
            {
              if (xx > hsize_) break;

              line     = do_align(block, line, xx, newalign, links);
              xx       = block->x;
              yy       += hh;
              block->h += hh;
              hh       = fsize + 2;
            }
            else
              xx += (int)fl_width(' ');

            if ((fsize + 2) > hh)
              hh = fsize + 2;

            ptr ++;
          }

          if (xx > hsize_) {
            hsize_ = xx;
            done   = 0;
            break;
          }

          needspace = 0;
        }
        else
        {
          // Handle normal text or stuff in the  section...
          while (isspace((*ptr)&255))
            ptr ++;
        }

        buf.clear();
      }

      if (*ptr == '<')
      {
        // Handle html tags..
        start = ptr;
        ptr ++;

        if (strncmp(ptr, "!--", 3) == 0)
        {
          // Comment...
          ptr += 3;
          if ((ptr = strstr(ptr, "-->")) != NULL)
          {
            ptr += 3;
            continue;
          }
          else
            break;
        }

        while (*ptr && *ptr != '>' && !isspace((*ptr)&255))
          buf += *ptr++;

        attrs = ptr;
        while (*ptr && *ptr != '>')
          ptr ++;

        if (*ptr == '>')
          ptr ++;

        if (buf.cmp("HEAD"))
          head = 1;
        else if (buf.cmp("/HEAD"))
          head = 0;
        else if (buf.cmp("TITLE"))
        {
          // Copy the title in the document...
          title_.clear();
          for ( ; *ptr != '<' && *ptr; ptr++) {
            title_.push_back(*ptr);
          }
          buf.clear();
        }
        else if (buf.cmp("A"))
        {
          if (get_attr(attrs, "NAME", attr, sizeof(attr)) != NULL)
            add_target(attr, yy - fsize - 2);

          if (get_attr(attrs, "HREF", attr, sizeof(attr)) != NULL)
            strlcpy(linkdest, attr, sizeof(linkdest));
        }
        else if (buf.cmp("/A"))
          linkdest[0] = '\0';
        else if (buf.cmp("BODY"))
        {
          bgcolor_   = get_color(get_attr(attrs, "BGCOLOR", attr, sizeof(attr)),
                                 color());
          textcolor_ = get_color(get_attr(attrs, "TEXT", attr, sizeof(attr)),
                                 textcolor());
          linkcolor_ = get_color(get_attr(attrs, "LINK", attr, sizeof(attr)),
                                 fl_contrast(FL_BLUE, color()));
        }
        else if (buf.cmp("BR"))
        {
          line     = do_align(block, line, xx, newalign, links);
          xx       = block->x;
          block->h += hh;
          yy       += hh;
          hh       = 0;
        }
        else if (buf.cmp("CENTER") ||
                 buf.cmp("P") ||
                 buf.cmp("H1") ||
                 buf.cmp("H2") ||
                 buf.cmp("H3") ||
                 buf.cmp("H4") ||
                 buf.cmp("H5") ||
                 buf.cmp("H6") ||
                 buf.cmp("UL") ||
                 buf.cmp("OL") ||
                 buf.cmp("DL") ||
                 buf.cmp("LI") ||
                 buf.cmp("DD") ||
                 buf.cmp("DT") ||
                 buf.cmp("HR") ||
                 buf.cmp("PRE") ||
                 buf.cmp("TABLE"))
        {
          block->end = start;
          line       = do_align(block, line, xx, newalign, links);
          newalign   = buf.cmp("CENTER") ? CENTER : LEFT;
          xx         = block->x;
          block->h   += hh;

          if (buf.cmp("OL")) {
            int ol_num = 1;
            if (get_attr(attrs, "START", attr, sizeof(attr)) != NULL) {
              errno = 0;
              char *endptr = 0;
              ol_num = (int)strtol(attr, &endptr, 10);
              if (errno || endptr == attr || ol_num < 0)
                ol_num = 1;
            }
            OL_num.push_back(ol_num);
          }
          else if (buf.cmp("UL"))
            OL_num.push_back(-1);

          if (buf.cmp("UL") ||
              buf.cmp("OL") ||
              buf.cmp("DL"))
          {
            block->h += fsize + 2;
            xx       = margins.push(4 * fsize);
          }
          else if (buf.cmp("TABLE"))
          {
            if (get_attr(attrs, "BORDER", attr, sizeof(attr)))
              border = (uchar)atoi(attr);
            else
              border = 0;

            tc = rc = get_color(get_attr(attrs, "BGCOLOR", attr, sizeof(attr)), bgcolor_);

            block->h += fsize + 2;

            format_table(&table_width, columns, start);

            if ((xx + table_width) > hsize_) {
#ifdef DEBUG
              printf("xx=%d, table_width=%d, hsize_=%d\n", xx, table_width,
                     hsize_);
#endif // DEBUG
              hsize_ = xx + table_width;
              done   = 0;
              break;
            }

            switch (get_align(attrs, talign))
            {
              default :
                  table_offset = 0;
                  break;

              case CENTER :
                  table_offset = (hsize_ - table_width) / 2 - textsize_;
                  break;

              case RIGHT :
                  table_offset = hsize_ - table_width - textsize_;
                  break;
            }

            column = 0;
          }

          if (tolower(buf[0]) == 'h' && isdigit(buf[1]))
          {
            font  = FL_HELVETICA_BOLD;
            fsize = textsize_ + '7' - buf[1];
          }
          else if (buf.cmp("DT"))
          {
            font  = textfont_ | FL_ITALIC;
            fsize = textsize_;
          }
          else if (buf.cmp("PRE"))
          {
            font  = FL_COURIER;
            fsize = textsize_;
            pre   = 1;
          }
          else
          {
            font  = textfont_;
            fsize = textsize_;
          }

          pushfont(font, fsize);

          yy = block->y + block->h;
          hh = 0;

          if ((tolower(buf[0]) == 'h' && isdigit(buf[1])) ||
              buf.cmp("DD") ||
              buf.cmp("DT") ||
              buf.cmp("P"))
            yy += fsize + 2;
          else if (buf.cmp("HR"))
          {
            hh += 2 * fsize;
            yy += fsize;
          }

          if (row)
            block = add_block(start, xx, yy, block->w, 0);
          else
            block = add_block(start, xx, yy, hsize_, 0);

          if (buf.cmp("LI")) {
            block->ol = 0;
            if (OL_num.size() && OL_num.back()>=0) {
              block->ol = 1;
              block->ol_num = (int)OL_num.back();
              int nnum = OL_num.pop_back() + 1;
              OL_num.push_back(nnum);
            }
          }

          needspace = 0;
          line      = 0;

          if (buf.cmp("CENTER"))
            newalign = talign = CENTER;
          else
            newalign = get_align(attrs, talign);
        }
        else if (buf.cmp("/CENTER") ||
                 buf.cmp("/P") ||
                 buf.cmp("/H1") ||
                 buf.cmp("/H2") ||
                 buf.cmp("/H3") ||
                 buf.cmp("/H4") ||
                 buf.cmp("/H5") ||
                 buf.cmp("/H6") ||
                 buf.cmp("/PRE") ||
                 buf.cmp("/UL") ||
                 buf.cmp("/OL") ||
                 buf.cmp("/DL") ||
                 buf.cmp("/TABLE"))
        {
          line       = do_align(block, line, xx, newalign, links);
          xx         = block->x;
          block->end = ptr;

          if (buf.cmp("/OL") ||
              buf.cmp("/UL")) {
            if (OL_num.size()) OL_num.pop_back();
          }

          if (buf.cmp("/UL") ||
              buf.cmp("/OL") ||
              buf.cmp("/DL"))
          {
            xx       = margins.pop();
            block->h += fsize + 2;
          }
          else if (buf.cmp("/TABLE"))
          {
            block->h += fsize + 2;
            xx       = margins.current();
          }
          else if (buf.cmp("/PRE"))
          {
            pre = 0;
            hh  = 0;
          }
          else if (buf.cmp("/CENTER"))
            talign = LEFT;

          popfont(font, fsize, fcolor);

          //#if defined(__GNUC__)
          //#warning FIXME this isspace & 255 test will probably not work on a utf8 stream... And we use it everywhere!
          //#endif /*__GNUC__*/
          while (isspace((*ptr)&255))
            ptr ++;

          block->h += hh;
          yy       += hh;

          if (tolower(buf[2]) == 'l')
            yy += fsize + 2;

          if (row)
            block = add_block(ptr, xx, yy, block->w, 0);
          else
            block = add_block(ptr, xx, yy, hsize_, 0);

          needspace = 0;
          hh        = 0;
          line      = 0;
          newalign  = talign;
        }
        else if (buf.cmp("TR"))
        {
          block->end = start;
          line       = do_align(block, line, xx, newalign, links);
          xx         = block->x;
          block->h   += hh;

          if (row)
          {
            yy = blocks_[row].y + blocks_[row].h;

            for (cell = &blocks_[row + 1]; cell <= block; cell ++)
              if ((cell->y + cell->h) > yy)
                yy = cell->y + cell->h;

            block = &blocks_[row];

            block->h = yy - block->y + 2;

            for (i = 0; i < column; i ++)
              if (cells[i])
              {
                cell = &blocks_[cells[i]];
                cell->h = block->h;
              }
          }

          memset(cells, 0, sizeof(cells));

          yy        = block->y + block->h - 4;
          hh        = 0;
          block     = add_block(start, xx, yy, hsize_, 0);
          row       = (int) (block - &blocks_[0]);
          needspace = 0;
          column    = 0;
          line      = 0;

          rc = get_color(get_attr(attrs, "BGCOLOR", attr, sizeof(attr)), tc);
        }
        else if (buf.cmp("/TR") && row)
        {
          line       = do_align(block, line, xx, newalign, links);
          block->end = start;
          block->h   += hh;
          talign     = LEFT;

          xx = blocks_[row].x;
          yy = blocks_[row].y + blocks_[row].h;

          for (cell = &blocks_[row + 1]; cell <= block; cell ++)
            if ((cell->y + cell->h) > yy)
              yy = cell->y + cell->h;

          block = &blocks_[row];

          block->h = yy - block->y + 2;

          for (i = 0; i < column; i ++)
            if (cells[i])
            {
              cell = &blocks_[cells[i]];
              cell->h = block->h;
            }

          yy        = block->y + block->h /*- 4*/;
          block     = add_block(start, xx, yy, hsize_, 0);
          needspace = 0;
          row       = 0;
          line      = 0;
        }
        else if ((buf.cmp("TD") ||
                  buf.cmp("TH")) && row)
        {
          int   colspan;                // COLSPAN attribute


          line       = do_align(block, line, xx, newalign, links);
          block->end = start;
          block->h   += hh;

          if (buf.cmp("TH"))
            font = textfont_ | FL_BOLD;
          else
            font = textfont_;

          fsize = textsize_;

          xx = blocks_[row].x + fsize + 3 + table_offset;
          for (i = 0; i < column; i ++)
            xx += columns[i] + 6;

          margins.push(xx - margins.current());

          if (get_attr(attrs, "COLSPAN", attr, sizeof(attr)) != NULL)
            colspan = atoi(attr);
          else
            colspan = 1;

          for (i = 0, ww = -6; i < colspan; i ++)
            ww += columns[column + i] + 6;

          if (block->end == block->start && blocks_.size() > 1)
          {
            blocks_.pop_back();
            block --;
          }

          pushfont(font, fsize);

          yy        = blocks_[row].y;
          hh        = 0;
          block     = add_block(start, xx, yy, xx + ww, 0, border);
          needspace = 0;
          line      = 0;
          newalign  = get_align(attrs, tolower(buf[1]) == 'h' ? CENTER : LEFT);
          talign    = newalign;

          cells[column] = (int) (block - &blocks_[0]);

          column += colspan;

          block->bgcolor = get_color(get_attr(attrs, "BGCOLOR", attr,
                                              sizeof(attr)), rc);
        }
        else if ((buf.cmp("/TD") ||
                  buf.cmp("/TH")) && row)
        {
          line = do_align(block, line, xx, newalign, links);
          popfont(font, fsize, fcolor);
          xx = margins.pop();
          talign = LEFT;
        }
        else if (buf.cmp("FONT"))
        {
          if (get_attr(attrs, "FACE", attr, sizeof(attr)) != NULL) {
            if (!strncasecmp(attr, "helvetica", 9) ||
                !strncasecmp(attr, "arial", 5) ||
                !strncasecmp(attr, "sans", 4)) font = FL_HELVETICA;
            else if (!strncasecmp(attr, "times", 5) ||
                     !strncasecmp(attr, "serif", 5)) font = FL_TIMES;
            else if (!strncasecmp(attr, "symbol", 6)) font = FL_SYMBOL;
            else font = FL_COURIER;
          }

          if (get_attr(attrs, "SIZE", attr, sizeof(attr)) != NULL) {
            if (isdigit(attr[0] & 255)) {
              // Absolute size
              fsize = (int)(textsize_ * pow(1.2, atoi(attr) - 3.0));
            } else {
              // Relative size
              fsize = (int)(fsize * pow(1.2, atoi(attr)));
            }
          }

          pushfont(font, fsize);
        }
        else if (buf.cmp("/FONT"))
          popfont(font, fsize, fcolor);
        else if (buf.cmp("B") ||
                 buf.cmp("STRONG"))
          pushfont(font |= FL_BOLD, fsize);
        else if (buf.cmp("I") ||
                 buf.cmp("EM"))
          pushfont(font |= FL_ITALIC, fsize);
        else if (buf.cmp("CODE") ||
                 buf.cmp("TT"))
          pushfont(font = FL_COURIER, fsize);
        else if (buf.cmp("KBD"))
          pushfont(font = FL_COURIER_BOLD, fsize);
        else if (buf.cmp("VAR"))
          pushfont(font = FL_COURIER_ITALIC, fsize);
        else if (buf.cmp("/B") ||
                 buf.cmp("/STRONG") ||
                 buf.cmp("/I") ||
                 buf.cmp("/EM") ||
                 buf.cmp("/CODE") ||
                 buf.cmp("/TT") ||
                 buf.cmp("/KBD") ||
                 buf.cmp("/VAR"))
          popfont(font, fsize, fcolor);
        else if (buf.cmp("IMG"))
        {
          Fl_Shared_Image       *img = 0;
          int           width;
          int           height;


          get_attr(attrs, "WIDTH", wattr, sizeof(wattr));
          get_attr(attrs, "HEIGHT", hattr, sizeof(hattr));
          width  = get_length(wattr);
          height = get_length(hattr);

          if (get_attr(attrs, "SRC", attr, sizeof(attr))) {
            img    = get_image(attr, width, height);
            width  = img->w();
            height = img->h();
          }

          ww = width;

          if (ww > hsize_) {
            hsize_ = ww;
            done   = 0;
            break;
          }

          if (needspace && xx > block->x)
            ww += (int)fl_width(' ');

          if ((xx + ww) > block->w)
          {
            line     = do_align(block, line, xx, newalign, links);
            xx       = block->x;
            yy       += hh;
            block->h += hh;
            hh       = 0;
          }

          if (linkdest[0])
            add_link(linkdest, xx, yy-fsize, ww, height);

          xx += ww;
          if ((height + 2) > hh)
            hh = height + 2;

          needspace = 0;
        }
        buf.clear();
      }
      else if (*ptr == '\n' && pre)
      {
        if (linkdest[0])
          add_link(linkdest, xx, yy - hh, ww, hh);

        if (xx > hsize_) {
          hsize_ = xx;
          done   = 0;
          break;
        }

        line      = do_align(block, line, xx, newalign, links);
        xx        = block->x;
        yy        += hh;
        block->h  += hh;
        needspace = 0;
        ptr ++;
      }
      else if (isspace((*ptr)&255))
      {
        needspace = 1;
        if ( pre ) {
          xx += (int)fl_width(' ');
        }
        ptr ++;
      }
      else if (*ptr == '&')
      {
        // Handle html '&' codes, eg. "&"
        ptr ++;

        int qch = quote_char(ptr);

        if (qch < 0)
          buf += '&';
        else {
          buf.add(qch);
          ptr = strchr(ptr, ';') + 1;
        }

        if ((fsize + 2) > hh)
          hh = fsize + 2;
      }
      else
      {
        buf += *ptr++;

        if ((fsize + 2) > hh)
          hh = fsize + 2;
      }
    }

    if (buf.size() > 0 && !head)
    {
      ww = buf.width();

  //    printf("line = %d, xx = %d, ww = %d, block->x = %d, block->w = %d\n",
  //       line, xx, ww, block->x, block->w);

      if (ww > hsize_) {
        hsize_ = ww;
        done   = 0;
        break;
      }

      if (needspace && xx > block->x)
        ww += (int)fl_width(' ');

      if ((xx + ww) > block->w)
      {
        line     = do_align(block, line, xx, newalign, links);
        xx       = block->x;
        yy       += hh;
        block->h += hh;
        hh       = 0;
      }

      if (linkdest[0])
        add_link(linkdest, xx, yy - fsize, ww, fsize);

      xx += ww;
    }

    do_align(block, line, xx, newalign, links);

    block->end = ptr;
    size_      = yy + hh;
  }

//  printf("margins.depth_=%d\n", margins.depth_);

  int dx = Fl::box_dw(b) - Fl::box_dx(b);
  int dy = Fl::box_dh(b) - Fl::box_dy(b);
  int ss = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
  int dw = Fl::box_dw(b) + ss;
  int dh = Fl::box_dh(b);

  if (hsize_ > (w() - dw)) {
    hscrollbar_.show();

    dh += ss;

    if (size_ < (h() - dh)) {
      scrollbar_.hide();
      hscrollbar_.resize(x() + Fl::box_dx(b), y() + h() - ss - dy,
                         w() - Fl::box_dw(b), ss);
    } else {
      scrollbar_.show();
      scrollbar_.resize(x() + w() - ss - dx, y() + Fl::box_dy(b),
                        ss, h() - ss - Fl::box_dh(b));
      hscrollbar_.resize(x() + Fl::box_dx(b), y() + h() - ss - dy,
                         w() - ss - Fl::box_dw(b), ss);
    }
  } else {
    hscrollbar_.hide();

    if (size_ < (h() - dh)) scrollbar_.hide();
    else {
      scrollbar_.resize(x() + w() - ss - dx, y() + Fl::box_dy(b),
                        ss, h() - Fl::box_dh(b));
      scrollbar_.show();
    }
  }

  // Reset scrolling if it needs to be...
  if (scrollbar_.visible()) {
    int temph = h() - Fl::box_dh(b);
    if (hscrollbar_.visible()) temph -= ss;
    if ((topline_ + temph) > size_) topline(size_ - temph);
    else topline(topline_);
  } else topline(0);

  if (hscrollbar_.visible()) {
    int tempw = w() - ss - Fl::box_dw(b);
    if ((leftline_ + tempw) > hsize_) leftline(hsize_ - tempw);
    else leftline(leftline_);
  } else leftline(0);
}


/** 
  \brief Format a table 
  \param[out] table_width Total width of the table
  \param[out] columns Array of column widths
  \param[in] table Pointer to the start of the table in the HTML text
  */
void
Fl_Help_View::format_table(
  int *table_width,
  int *columns,
  const char *table)
{
  int           column,                                 // Current column
                num_columns,                            // Number of columns
                colspan,                                // COLSPAN attribute
                width,                                  // Current width
                temp_width,                             // Temporary width
                max_width,                              // Maximum width
                incell,                                 // In a table cell?
                pre,                                    // 
 text?
                needspace;                              // Need whitespace?
  HV_Edit_Buffer buf;                                   // Text buffer
  char          attr[1024],                             // Other attribute
                wattr[1024],                            // WIDTH attribute
                hattr[1024];                            // HEIGHT attribute
  const char    *ptr,                                   // Pointer into table
                *attrs,                                 // Pointer to attributes
                *start;                                 // Start of element
  int           minwidths[MAX_COLUMNS];                 // Minimum widths for each column
  Fl_Font       font;
  Fl_Fontsize   fsize;                                  // Current font and size
  Fl_Color      fcolor;                                 // Currrent font color

  DEBUG_FUNCTION(__LINE__,__FUNCTION__);

  // Clear widths...
  *table_width = 0;
  for (column = 0; column < MAX_COLUMNS; column ++)
  {
    columns[column]   = 0;
    minwidths[column] = 0;
  }

  num_columns = 0;
  colspan     = 0;
  max_width   = 0;
  pre         = 0;
  needspace   = 0;
  fstack_.top(font, fsize, fcolor);

  // Scan the table...
  for (ptr = table, column = -1, width = 0, incell = 0; *ptr;)
  {
    if ((*ptr == '<' || isspace((*ptr)&255)) && buf.size() > 0 && incell)
    {
      // Check width...
      if (needspace)
      {
        buf += ' ';
        needspace = 0;
      }

      temp_width = buf.width();
      buf.clear();

      if (temp_width > minwidths[column])
        minwidths[column] = temp_width;

      width += temp_width;

      if (width > max_width)
        max_width = width;
    }

    if (*ptr == '<')
    {
      start = ptr;

      for (buf.clear(), ptr ++; *ptr && *ptr != '>' && !isspace((*ptr)&255);)
        buf += *ptr++;

      attrs = ptr;
      while (*ptr && *ptr != '>')
        ptr ++;

      if (*ptr == '>')
        ptr ++;

      if (buf.cmp("BR") ||
          buf.cmp("HR"))
      {
        width     = 0;
        needspace = 0;
      }
      else if (buf.cmp("TABLE") && start > table)
        break;
      else if (buf.cmp("CENTER") ||
               buf.cmp("P") ||
               buf.cmp("H1") ||
               buf.cmp("H2") ||
               buf.cmp("H3") ||
               buf.cmp("H4") ||
               buf.cmp("H5") ||
               buf.cmp("H6") ||
               buf.cmp("UL") ||
               buf.cmp("OL") ||
               buf.cmp("DL") ||
               buf.cmp("LI") ||
               buf.cmp("DD") ||
               buf.cmp("DT") ||
               buf.cmp("PRE"))
      {
        width     = 0;
        needspace = 0;

        if (tolower(buf[0]) == 'h' && isdigit(buf[1]))
        {
          font  = FL_HELVETICA_BOLD;
          fsize = textsize_ + '7' - buf[1];
        }
        else if (buf.cmp("DT"))
        {
          font  = textfont_ | FL_ITALIC;
          fsize = textsize_;
        }
        else if (buf.cmp("PRE"))
        {
          font  = FL_COURIER;
          fsize = textsize_;
          pre   = 1;
        }
        else if (buf.cmp("LI"))
        {
          width  += 4 * fsize;
          font   = textfont_;
          fsize  = textsize_;
        }
        else
        {
          font  = textfont_;
          fsize = textsize_;
        }

        pushfont(font, fsize);
      }
      else if (buf.cmp("/CENTER") ||
               buf.cmp("/P") ||
               buf.cmp("/H1") ||
               buf.cmp("/H2") ||
               buf.cmp("/H3") ||
               buf.cmp("/H4") ||
               buf.cmp("/H5") ||
               buf.cmp("/H6") ||
               buf.cmp("/PRE") ||
               buf.cmp("/UL") ||
               buf.cmp("/OL") ||
               buf.cmp("/DL"))
      {
        width     = 0;
        needspace = 0;

        popfont(font, fsize, fcolor);
      }
      else if (buf.cmp("TR") || buf.cmp("/TR") ||
               buf.cmp("/TABLE"))
      {
//        printf("%s column = %d, colspan = %d, num_columns = %d\n",
//             buf.c_str(), column, colspan, num_columns);

        if (column >= 0)
        {
          // This is a hack to support COLSPAN...
          max_width /= colspan;

          while (colspan > 0)
          {
            if (max_width > columns[column])
              columns[column] = max_width;

            column ++;
            colspan --;
          }
        }

        if (buf.cmp("/TABLE"))
          break;

        needspace = 0;
        column    = -1;
        width     = 0;
        max_width = 0;
        incell    = 0;
      }
      else if (buf.cmp("TD") ||
               buf.cmp("TH"))
      {
//        printf("BEFORE column = %d, colspan = %d, num_columns = %d\n",
//             column, colspan, num_columns);

        if (column >= 0)
        {
          // This is a hack to support COLSPAN...
          max_width /= colspan;

          while (colspan > 0)
          {
            if (max_width > columns[column])
              columns[column] = max_width;

            column ++;
            colspan --;
          }
        }
        else
          column ++;

        if (get_attr(attrs, "COLSPAN", attr, sizeof(attr)) != NULL)
          colspan = atoi(attr);
        else
          colspan = 1;

//        printf("AFTER column = %d, colspan = %d, num_columns = %d\n",
//             column, colspan, num_columns);

        if ((column + colspan) >= num_columns)
          num_columns = column + colspan;

        needspace = 0;
        width     = 0;
        incell    = 1;

        if (buf.cmp("TH"))
          font = textfont_ | FL_BOLD;
        else
          font = textfont_;

        fsize = textsize_;

        pushfont(font, fsize);

        if (get_attr(attrs, "WIDTH", attr, sizeof(attr)) != NULL)
          max_width = get_length(attr);
        else
          max_width = 0;

//        printf("max_width = %d\n", max_width);
      }
      else if (buf.cmp("/TD") ||
               buf.cmp("/TH"))
      {
        incell = 0;
        popfont(font, fsize, fcolor);
      }
      else if (buf.cmp("B") ||
               buf.cmp("STRONG"))
        pushfont(font |= FL_BOLD, fsize);
      else if (buf.cmp("I") ||
               buf.cmp("EM"))
        pushfont(font |= FL_ITALIC, fsize);
      else if (buf.cmp("CODE") ||
               buf.cmp("TT"))
        pushfont(font = FL_COURIER, fsize);
      else if (buf.cmp("KBD"))
        pushfont(font = FL_COURIER_BOLD, fsize);
      else if (buf.cmp("VAR"))
        pushfont(font = FL_COURIER_ITALIC, fsize);
      else if (buf.cmp("/B") ||
               buf.cmp("/STRONG") ||
               buf.cmp("/I") ||
               buf.cmp("/EM") ||
               buf.cmp("/CODE") ||
               buf.cmp("/TT") ||
               buf.cmp("/KBD") ||
               buf.cmp("/VAR"))
        popfont(font, fsize, fcolor);
      else if (buf.cmp("IMG") && incell)
      {
        Fl_Shared_Image *img = 0;
        int             iwidth, iheight;


        get_attr(attrs, "WIDTH", wattr, sizeof(wattr));
        get_attr(attrs, "HEIGHT", hattr, sizeof(hattr));
        iwidth  = get_length(wattr);
        iheight = get_length(hattr);

        if (get_attr(attrs, "SRC", attr, sizeof(attr))) {
          img     = get_image(attr, iwidth, iheight);
          iwidth  = img->w();
          iheight = img->h();
        }

        if (iwidth > minwidths[column])
          minwidths[column] = iwidth;

        width += iwidth;
        if (needspace)
          width += (int)fl_width(' ');

        if (width > max_width)
          max_width = width;

        needspace = 0;
      }
      buf.clear();
    }
    else if (*ptr == '\n' && pre)
    {
      width     = 0;
      needspace = 0;
      ptr ++;
    }
    else if (isspace((*ptr)&255))
    {
      needspace = 1;

      ptr ++;
    }
    else if (*ptr == '&' )
    {
      ptr ++;

      int qch = quote_char(ptr);

      if (qch < 0)
        buf += '&';
      else {
        buf.add(qch);
        ptr = strchr(ptr, ';') + 1;
      }
    }
    else
    {
      buf += *ptr++;
    }
  }

  // Now that we have scanned the entire table, adjust the table and
  // cell widths to fit on the screen...
  if (get_attr(table + 6, "WIDTH", attr, sizeof(attr)))
    *table_width = get_length(attr);
  else
    *table_width = 0;

#ifdef DEBUG
  printf("num_columns = %d, table_width = %d\n", num_columns, *table_width);
#endif // DEBUG

  if (num_columns == 0)
    return;

  // Add up the widths...
  for (column = 0, width = 0; column < num_columns; column ++)
    width += columns[column];

#ifdef DEBUG
  printf("width = %d, w() = %d\n", width, w());
  for (column = 0; column < num_columns; column ++)
    printf("    columns[%d] = %d, minwidths[%d] = %d\n", column, columns[column],
           column, minwidths[column]);
#endif // DEBUG

  // Adjust the width if needed...
  int scale_width = *table_width;

  int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
  if (scale_width == 0) {
    if (width > (hsize_ - scrollsize)) scale_width = hsize_ - scrollsize;
    else scale_width = width;
  }

  if (width < scale_width) {
#ifdef DEBUG
    printf("Scaling table up to %d from %d...\n", scale_width, width);
#endif // DEBUG

    *table_width = 0;

    scale_width = (scale_width - width) / num_columns;

#ifdef DEBUG
    printf("adjusted scale_width = %d\n", scale_width);
#endif // DEBUG

    for (column = 0; column < num_columns; column ++) {
      columns[column] += scale_width;

      (*table_width) += columns[column];
    }
  }
  else if (width > scale_width) {
#ifdef DEBUG
    printf("Scaling table down to %d from %d...\n", scale_width, width);
#endif // DEBUG

    for (column = 0; column < num_columns; column ++) {
      width       -= minwidths[column];
      scale_width -= minwidths[column];
    }

#ifdef DEBUG
    printf("adjusted width = %d, scale_width = %d\n", width, scale_width);
#endif // DEBUG

    if (width > 0) {
      for (column = 0; column < num_columns; column ++) {
        columns[column] -= minwidths[column];
        columns[column] = scale_width * columns[column] / width;
        columns[column] += minwidths[column];
      }
    }

    *table_width = 0;
    for (column = 0; column < num_columns; column ++) {
      (*table_width) += columns[column];
    }
  }
  else if (*table_width == 0)
    *table_width = width;

#ifdef DEBUG
  printf("FINAL table_width = %d\n", *table_width);
  for (column = 0; column < num_columns; column ++)
    printf("    columns[%d] = %d\n", column, columns[column]);
#endif // DEBUG
}


/** 
  \brief Frees memory used for the document. 
  */
void Fl_Help_View::free_data() {
  // Release all images...
  if (value_) {
    const char  *ptr,           // Pointer into block
                *attrs;         // Pointer to start of element attributes
    HV_Edit_Buffer buf;         // Text buffer
    char        attr[1024],     // Attribute buffer
                wattr[1024],    // Width attribute buffer
                hattr[1024];    // Height attribute buffer

    DEBUG_FUNCTION(__LINE__,__FUNCTION__);

    for (ptr = value_; *ptr;)
    {
      if (*ptr == '<')
      {
        ptr ++;

        if (strncmp(ptr, "!--", 3) == 0)
        {
          // Comment...
          ptr += 3;
          if ((ptr = strstr(ptr, "-->")) != NULL)
          {
            ptr += 3;
            continue;
          }
          else
            break;
        }

        buf.clear();

        while (*ptr && *ptr != '>' && !isspace((*ptr)&255))
          buf += *ptr++;

        attrs = ptr;
        while (*ptr && *ptr != '>')
          ptr ++;

        if (*ptr == '>')
          ptr ++;

        if (buf.cmp("IMG"))
        {
          Fl_Shared_Image       *img;
          int           width;
          int           height;

          get_attr(attrs, "WIDTH", wattr, sizeof(wattr));
          get_attr(attrs, "HEIGHT", hattr, sizeof(hattr));
          width  = get_length(wattr);
          height = get_length(hattr);

          if (get_attr(attrs, "SRC", attr, sizeof(attr))) {
            // Get and release the image to free it from memory...
            img = get_image(attr, width, height);
            if ((void*)img != &broken_image) {
              img->release();
            }
          }
        }
      }
      else
        ptr++;
    }

    free((void *)value_);
    value_ = 0;
  }

  blocks_ .clear();
  link_list_.clear();
  target_line_map_.clear();
}


/** 
  \brief Gets an alignment attribute. 
  \param[in] p Pointer to start of attributes.
  \param[in] a Default alignment.
  \return Alignment value, either CENTER, RIGHT, or LEFT.
*/
int Fl_Help_View::get_align(const char *p, int a)
{
  char  buf[255];                       // Alignment value

  if (get_attr(p, "ALIGN", buf, sizeof(buf)) == NULL)
    return (a);

  if (strcasecmp(buf, "CENTER") == 0)
    return (CENTER);
  else if (strcasecmp(buf, "RIGHT") == 0)
    return (RIGHT);
  else
    return (LEFT);
}


/** 
  \brief Gets an attribute value from the string. 
  \param[in] p Pointer to start of attributes.
  \param[in] n Name of attribute.
  \param[out] buf Buffer for attribute value.
  \param[in] bufsize Size of buffer.
  \return Pointer to buf or NULL if not found.
  */
const char *Fl_Help_View::get_attr(
  const char *p,
  const char *n,
  char *buf,
  int bufsize)
{
  char  name[255],                              // Name from string
        *ptr,                                   // Pointer into name or value
        quote;                                  // Quote


  buf[0] = '\0';

  while (*p && *p != '>')
  {
    while (isspace((*p)&255))
      p ++;

    if (*p == '>' || !*p)
      return (NULL);

    for (ptr = name; *p && !isspace((*p)&255) && *p != '=' && *p != '>';)
      if (ptr < (name + sizeof(name) - 1))
        *ptr++ = *p++;
      else
        p ++;

    *ptr = '\0';

    if (isspace((*p)&255) || !*p || *p == '>')
      buf[0] = '\0';
    else
    {
      if (*p == '=')
        p ++;

      for (ptr = buf; *p && !isspace((*p)&255) && *p != '>';)
        if (*p == '\'' || *p == '\"')
        {
          quote = *p++;

          while (*p && *p != quote)
            if ((ptr - buf + 1) < bufsize)
              *ptr++ = *p++;
            else
              p ++;

          if (*p == quote)
            p ++;
        }
        else if ((ptr - buf + 1) < bufsize)
          *ptr++ = *p++;
        else
          p ++;

      *ptr = '\0';
    }

    if (strcasecmp(n, name) == 0)
      return (buf);
    else
      buf[0] = '\0';

    if (*p == '>')
      return (NULL);
  }

  return (NULL);
}


/** 
  \brief Gets a color attribute. 
  \param[in] n the color name, either a name or a hex value.
  \param[in] c the default color value.
  \return the color value, either the color from the name or the default value.
  */
Fl_Color Fl_Help_View::get_color(const char *n, Fl_Color c)
{
  int   i;                              // Looping var
  int   rgb, r, g, b;                   // RGB values
  static const struct {                 // Color name table
    const char *name;
    int r, g, b;
  }     colors[] = {
    { "black",          0x00, 0x00, 0x00 },
    { "red",            0xff, 0x00, 0x00 },
    { "green",          0x00, 0x80, 0x00 },
    { "yellow",         0xff, 0xff, 0x00 },
    { "blue",           0x00, 0x00, 0xff },
    { "magenta",        0xff, 0x00, 0xff },
    { "fuchsia",        0xff, 0x00, 0xff },
    { "cyan",           0x00, 0xff, 0xff },
    { "aqua",           0x00, 0xff, 0xff },
    { "white",          0xff, 0xff, 0xff },
    { "gray",           0x80, 0x80, 0x80 },
    { "grey",           0x80, 0x80, 0x80 },
    { "lime",           0x00, 0xff, 0x00 },
    { "maroon",         0x80, 0x00, 0x00 },
    { "navy",           0x00, 0x00, 0x80 },
    { "olive",          0x80, 0x80, 0x00 },
    { "purple",         0x80, 0x00, 0x80 },
    { "silver",         0xc0, 0xc0, 0xc0 },
    { "teal",           0x00, 0x80, 0x80 }
  };


  if (!n || !n[0]) return c;

  if (n[0] == '#') {
    // Do hex color lookup
    rgb = (int)strtol(n + 1, NULL, 16);

    if (strlen(n) > 4) {
      r = rgb >> 16;
      g = (rgb >> 8) & 255;
      b = rgb & 255;
    } else {
      r = (rgb >> 8) * 17;
      g = ((rgb >> 4) & 15) * 17;
      b = (rgb & 15) * 17;
    }
    return (fl_rgb_color((uchar)r, (uchar)g, (uchar)b));
  } else {
    for (i = 0; i < (int)(sizeof(colors) / sizeof(colors[0])); i ++)
      if (!strcasecmp(n, colors[i].name)) {
        return fl_rgb_color(colors[i].r, colors[i].g, colors[i].b);
      }
    return c;
  }
}


/* Implementation note: (A.S. Apr 05, 2009)

  Fl_Help_View::get_image() uses a static global flag (initial_load)
  to determine, if it is called from the initial loading of a document
  (load() or value()), or from resize() or draw().

  A better solution would be to manage all loaded images in an own
  structure like Fl_Help_Target (Fl_Help_Image ?) to avoid using this
  global flag, but this would break the ABI !

  This should be fixed in FLTK 1.3 !


  If initial_load is true, then Fl_Shared_Image::get() is called to
  load the image, and the reference count of the shared image is
  increased by one.

  If initial_load is false, then Fl_Shared_Image::find() is called to
  load the image, and the image is released immediately. This avoids
  increasing the reference count when calling get_image() from draw()
  or resize().

  Calling Fl_Shared_Image::find() instead of Fl_Shared_Image::get() avoids
  doing unnecessary i/o for "broken images" within each resize/redraw.

  Each image must be released exactly once in the destructor or before
  a new document is loaded: see free_data().
*/

/** 
  \brief Gets an inline image.

  The image reference count is maintained accordingly, such that
  the image can be released exactly once when the document is closed.

  \param[in] name the image name, either a local filename or a URL.
  \param[in] W, H the size of the image, or 0 if not specified.
  \return a pointer to a cached Fl_Shared_Image, if the image can be loaded,
          otherwise a pointer to an internal Fl_Pixmap (broken_image).

  \todo Fl_Help_View::get_image() returns a pointer to the internal
  Fl_Pixmap broken_image, but this is _not_ compatible with the
  return type Fl_Shared_Image (release() must not be called).
*/
Fl_Shared_Image *Fl_Help_View::get_image(const char *name, int W, int H) 
{
  const char    *localname;             // Local filename
  char          dir[FL_PATH_MAX];       // Current directory // TODO: make std::string
  char          temp[3 * FL_PATH_MAX],  // Temporary filename // TODO: make std::string
                *tempptr;               // Pointer into temporary name
  Fl_Shared_Image *ip;                  // Image pointer...

  // See if the image can be found...
  if (strchr(directory_.c_str(), ':') != NULL && strchr(name, ':') == NULL) {
    if (name[0] == '/') {
      strlcpy(temp, directory_.c_str(), sizeof(temp));
      // the following search (strchr) will always succeed, see condition above!
      tempptr = skip_bytes(strchr(temp, ':'), 3);
      if ((tempptr = strrchr(tempptr, '/')) != NULL) {
        strlcpy(tempptr, name, sizeof(temp) - (tempptr - temp));
      } else {
        strlcat(temp, name, sizeof(temp));
      }
    } else {
      snprintf(temp, sizeof(temp), "%s/%s", directory_.c_str(), name);
    }

    if (link_) localname = (*link_)(this, temp);
    else localname = temp;
  } else if (name[0] != '/' && strchr(name, ':') == NULL) {
    if (!directory_.empty()) {
      snprintf(temp, sizeof(temp), "%s/%s", directory_.c_str(), name);
    } else {
      fl_getcwd(dir, sizeof(dir));
      snprintf(temp, sizeof(temp), "file:%s/%s", dir, name);
    }

    if (link_) localname = (*link_)(this, temp);
    else localname = temp;
  } else if (link_) localname = (*link_)(this, name);
  else localname = name;

  if (!localname) return 0;

  if (strncmp(localname, "file:", 5) == 0) localname += 5;

  if (initial_load) {
    if ((ip = Fl_Shared_Image::get(localname, W, H)) == NULL) {
      ip = (Fl_Shared_Image *)&broken_image;
    }
  } else { // draw or resize
    if ((ip = Fl_Shared_Image::find(localname, W, H)) == NULL) {
      ip = (Fl_Shared_Image *)&broken_image;
    } else {
      ip->release();
    }
  }

  return ip;
}


/** 
  \brief Gets a length value, either absolute or %. 
  \param[in] l string containing the length value
  \return the length in pixels, or 0 if the string is empty.
*/
int Fl_Help_View::get_length(const char *l) {
  int val;

  if (!l[0]) return 0;

  val = atoi(l);
  if (l[strlen(l) - 1] == '%') {
    if (val > 100) val = 100;
    else if (val < 0) val = 0;

    int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
    val = val * (hsize_ - scrollsize) / 100;
  }

  return val;
}


std::shared_ptr Fl_Help_View::find_link(int xx, int yy)
{
  for (auto &link : link_list_) {
    if (link->box.contains(xx, yy)) {
      return link;
    }
  }
  return nullptr;
}


void Fl_Help_View::follow_link(std::shared_ptr linkp)
{
  std::string target;     // Current target

  clear_selection();

  target = linkp->target;

  set_changed();

  if (strcmp(linkp->filename_.c_str(), filename_.c_str()) != 0 && !linkp->filename_.empty())
  {
    char        temp[3 * FL_PATH_MAX],  // Temporary filename // TODO: make std::string
               *tempptr;                // Pointer into temporary filename

    if (strchr(directory_.c_str(), ':') != NULL && strchr(linkp->filename_.c_str(), ':') == NULL)
    {
      if (linkp->filename_[0] == '/')
      {
        strlcpy(temp, directory_.c_str(), sizeof(temp));
        // the following search (strchr) will always succeed, see condition above!
        tempptr = skip_bytes(strchr(temp, ':'), 3);
        if ((tempptr = strrchr(tempptr, '/')) != NULL) {
          strlcpy(tempptr, linkp->filename_.c_str(), sizeof(temp) - (tempptr - temp));
        } else {
          strlcat(temp, linkp->filename_.c_str(), sizeof(temp));
        }
      }
      else
        snprintf(temp, sizeof(temp), "%s/%s", directory_.c_str(), linkp->filename_.c_str());
    }
    else if (linkp->filename_[0] != '/' && strchr(linkp->filename_.c_str(), ':') == NULL)
    {
      if (!directory_.empty())
        snprintf(temp, sizeof(temp), "%s/%s", directory_.c_str(), linkp->filename_.c_str());
      else
      {
        char dir[FL_PATH_MAX];       // Current directory (static size ok until we have fl_getcwd_std()
        fl_getcwd(dir, sizeof(dir));
        snprintf(temp, sizeof(temp), "file:%s/%s", dir, linkp->filename_.c_str());
      }
    }
    else
      strlcpy(temp, linkp->filename_.c_str(), sizeof(temp));

    if (!linkp->target.empty()) {
      snprintf(temp + strlen(temp), sizeof(temp) - strlen(temp), "#%s", linkp->target.c_str());
    }

    load(temp);
  }
  else if (!target.empty()) 
    topline(target.c_str());
  else
    topline(0);

  leftline(0);
}


/** 
  \brief Removes the current text selection. 
*/
void Fl_Help_View::clear_selection()
{
  if (current_view_==this)
    clear_global_selection();
}


/** 
  \brief Selects all the text in the view. 
*/
void Fl_Help_View::select_all()
{
  clear_global_selection();
  if (!value_) return;
  current_view_ = this;
  selection_drag_last_ = selection_last_ = (int) strlen(value_);
  selected_ = 1;
}


void Fl_Help_View::clear_global_selection()
{
  if (selected_) redraw();
  selection_push_first_ = selection_push_last_ = 0;
  selection_drag_first_ = selection_drag_last_ = 0;
  selection_first_ = selection_last_ = 0;
  selected_ = 0;
}


char Fl_Help_View::begin_selection()
{
  clear_global_selection();

  if (!fl_help_view_buffer) fl_help_view_buffer = fl_create_offscreen(1, 1);

  mouse_x_ = Fl::event_x();
  mouse_y_ = Fl::event_y();
  draw_mode_ = 1;

    current_view_ = this;
    fl_begin_offscreen(fl_help_view_buffer);
    draw();
    fl_end_offscreen();

  draw_mode_ = 0;

  if (selection_push_last_) return 1;
  else return 0;
}


char Fl_Help_View::extend_selection()
{
  if (Fl::event_is_click())
    return 0;

  // Give this widget the focus during the selection process. This will
  // deselect other text selection and make sure, we receive the Copy
  // keyboard shortcut.
  if (Fl::focus()!=this)
    Fl::focus(this);

//  printf("old selection_first_=%d, selection_last_=%d\n",
//         selection_first_, selection_last_);

  int sf = selection_first_, sl = selection_last_;

  selected_ = 1;
  mouse_x_ = Fl::event_x();
  mouse_y_ = Fl::event_y();
  draw_mode_ = 2;

    fl_begin_offscreen(fl_help_view_buffer);
    draw();
    fl_end_offscreen();

  draw_mode_ = 0;

  if (selection_push_first_ < selection_drag_first_) {
    selection_first_ = selection_push_first_;
  } else {
    selection_first_ = selection_drag_first_;
  }

  if (selection_push_last_ > selection_drag_last_) {
    selection_last_ = selection_push_last_;
  } else {
    selection_last_ = selection_drag_last_;
  }

//  printf("new selection_first_=%d, selection_last_=%d\n",
//         selection_first_, selection_last_);

  if (sf!=selection_first_ || sl!=selection_last_) {
//    puts("REDRAW!!!\n");
    return 1;
  } else {
//    puts("");
    return 0;
  }
}

// convert a command with up to four letters into an unsigned int
static uint32_t command(const char *cmd)
{
  uint32_t ret = (tolower(cmd[0])<<24);
  char c = cmd[1];
  if (c=='>' || c==' ' || c==0) return ret;
  ret |= (tolower(c)<<16);
  c = cmd[2];
  if (c=='>' || c==' ' || c==0) return ret;
  ret |= (tolower(c)<<8);
  c = cmd[3];
  if (c=='>' || c==' ' || c==0) return ret;
  ret |= tolower(c);
  c = cmd[4];
  if (c=='>' || c==' ' || c==0) return ret;
  return 0;
}


static constexpr uint32_t CMD(char a, char b, char c, char d) 
{ 
  return ((a<<24)|(b<<16)|(c<<8)|d); 
}


void Fl_Help_View::end_selection(int clipboard)
{
  if (!selected_ || current_view_!=this)
    return;
  // convert the select part of our html text into some kind of somewhat readable UTF-8
  // and store it in the selection buffer
  int p = 0;
  char pre = 0;
  int len = (int) strlen(value_);
  char *txt = (char*)malloc(len+1), *d = txt;
  const char *s = value_, *cmd, *src;
  for (;;) {
    int c = (*s++) & 0xff;
    if (c==0) break;
    if (c=='<') { // begin of some html command. Skip until we find a '>'
      cmd = s;
      for (;;) {
        c = (*s++) & 0xff;
        if (c==0 || c=='>') break;
      }
      if (c==0) break;
      // do something with this command... .
      // The replacement string must not be longer than the command
      // itself plus '<' and '>'
      src = 0;
      switch (command(cmd)) {
        case CMD('p','r','e', 0 ): pre = 1; break;
        case CMD('/','p','r','e'): pre = 0; break;
        case CMD('t','d', 0 , 0 ):
        case CMD('p', 0 , 0 , 0 ):
        case CMD('/','p', 0 , 0 ):
        case CMD('b','r', 0 , 0 ): src = "\n"; break;
        case CMD('l','i', 0 , 0 ): src = "\n * "; break;
        case CMD('/','h','1', 0 ):
        case CMD('/','h','2', 0 ):
        case CMD('/','h','3', 0 ):
        case CMD('/','h','4', 0 ):
        case CMD('/','h','5', 0 ):
        case CMD('/','h','6', 0 ): src = "\n\n"; break;
        case CMD('t','r', 0 , 0 ):
        case CMD('h','1', 0 , 0 ):
        case CMD('h','2', 0 , 0 ):
        case CMD('h','3', 0 , 0 ):
        case CMD('h','4', 0 , 0 ):
        case CMD('h','5', 0 , 0 ):
        case CMD('h','6', 0 , 0 ): src = "\n\n"; break;
        case CMD('d','t', 0 , 0 ): src = "\n "; break;
        case CMD('d','d', 0 , 0 ): src = "\n - "; break;
      }
      int n = (int) (s-value_);
      if (src && n>selection_first_ && n<=selection_last_) {
        while (*src) {
          *d++ = *src++;
        }
        c = src[-1] & 0xff;
        p = isspace(c) ? ' ' : c;
      }
      continue;
    }
    const char *s2 = s;
    if (c=='&') { // special characters (HTML entities)
      int xx = quote_char(s);
      if (xx >= 0) {
        c = xx;
        for (;;) {
          char cc = *s++;
          if (!cc || cc==';') break;
        }
      }
    }
    int n = (int) (s2-value_);
    if (n>selection_first_ && n<=selection_last_) {
      if (!pre && c < 256 && isspace(c)) c = ' ';
      if (p != ' ' || c != ' ') {
        if (s2 != s) { // c was an HTML entity
          d += fl_utf8encode(c, d);
        }
        else *d++ = c;
      }
      p = c;
    }
    if (n>selection_last_) break; // stop parsing html after end of selection
  }
  *d = 0;
  Fl::copy(txt, (int) strlen(txt), clipboard);
  // printf("copy [%s]\n", txt);
  free(txt);
}


/**
 \brief Check if the user selected text in this view.
 \return 1 if text is selected, 0 if no text is selected
 */
int Fl_Help_View::text_selected() {
  if (current_view_==this)
    return selected_;
  else
    return 0;
}


/**
 \brief If text is selected in this view, copy it to a clipboard.
 \param[in] clipboard for x11 only, 0=selection buffer, 1=clipboard, 2=both
 \return 1 if text is selected, 0 if no text is selected
 */
int Fl_Help_View::copy(int clipboard) {
  if (text_selected()) {
    end_selection(clipboard);
    return 1;
  } else {
    return 0;
  }
}


/** 
  \brief Handles events in the widget. 
  \param[in] event Event to handle.
  \return 1 if the event was handled, 0 otherwise.
  */
int Fl_Help_View::handle(int event)
{
  static std::shared_ptr linkp = nullptr;   // currently clicked link

  int xx = Fl::event_x() - x() + leftline_;
  int yy = Fl::event_y() - y() + topline_;

  switch (event)
  {
    case FL_FOCUS:
      redraw();
      return 1;
    case FL_UNFOCUS:
      clear_selection();
      redraw();
      return 1;
    case FL_ENTER :
      Fl_Group::handle(event);
      return 1;
    case FL_LEAVE :
      fl_cursor(FL_CURSOR_DEFAULT);
      break;
    case FL_MOVE:
      if (find_link(xx, yy)) fl_cursor(FL_CURSOR_HAND);
      else fl_cursor(FL_CURSOR_DEFAULT);
      return 1;
    case FL_PUSH:
      if (Fl::event_button() == FL_RIGHT_MOUSE) {
        rmb_menu[0].label(copy_menu_text);
        if (text_selected())
          rmb_menu[0].activate();
        else
          rmb_menu[0].deactivate();
        fl_cursor(FL_CURSOR_DEFAULT);
        const Fl_Menu_Item *mi = rmb_menu->popup(Fl::event_x(), Fl::event_y());
        if (mi) switch (mi->argument()) {
          case 1:
            copy();
            break;
        }
      }
      if (Fl_Group::handle(event)) return 1;
      linkp = find_link(xx, yy);
      if (linkp) {
        fl_cursor(FL_CURSOR_HAND);
        return 1;
      }
      if (begin_selection()) {
        fl_cursor(FL_CURSOR_INSERT);
        return 1;
      }
      fl_cursor(FL_CURSOR_DEFAULT);
      return 1;
    case FL_DRAG:
      if (linkp) {
        if (Fl::event_is_click()) {
          fl_cursor(FL_CURSOR_HAND);
        } else {
          fl_cursor(FL_CURSOR_DEFAULT); // should be "FL_CURSOR_CANCEL" if we had it
        }
        return 1;
      }
      if (current_view_==this && selection_push_last_) {
        if (extend_selection()) redraw();
        fl_cursor(FL_CURSOR_INSERT);
        return 1;
      }
      fl_cursor(FL_CURSOR_DEFAULT);
      return 1;
    case FL_RELEASE:
      if (linkp) {
        if (Fl::event_is_click()) {
          follow_link(linkp);
        }
        fl_cursor(FL_CURSOR_DEFAULT);
        linkp = 0;
        return 1;
      }
      if (current_view_==this && selection_push_last_) {
        end_selection();
        return 1;
      }
      return 1;
    case FL_SHORTCUT: {
      int mods = Fl::event_state() & (FL_META|FL_CTRL|FL_ALT|FL_SHIFT);
      if ( mods == FL_COMMAND) {
        switch ( Fl::event_key() ) {
          case 'a': select_all(); redraw(); return 1;
          case 'c':
          case 'x': end_selection(1); return 1;
        }
      }
      break; }
  }
  return (Fl_Group::handle(event));
}


/**
  \brief Creates the Fl_Help_View widget at the specified position and size.
  \param[in] xx, yy, ww, hh Position and size of the widget
  \param[in] l Label for the widget, can be NULL
*/
Fl_Help_View::Fl_Help_View(int xx, int yy, int ww, int hh, const char *l)
: Fl_Group(xx, yy, ww, hh, l),
  scrollbar_(xx + ww - Fl::scrollbar_size(), yy, Fl::scrollbar_size(), hh - Fl::scrollbar_size()),
  hscrollbar_(xx, yy + hh - Fl::scrollbar_size(), ww - Fl::scrollbar_size(), Fl::scrollbar_size())
{
  color(FL_BACKGROUND2_COLOR, FL_SELECTION_COLOR);

  title_[0]     = '\0';
  defcolor_     = FL_FOREGROUND_COLOR;
  bgcolor_      = FL_BACKGROUND_COLOR;
  textcolor_    = FL_FOREGROUND_COLOR;
  linkcolor_    = FL_SELECTION_COLOR;
  textfont_     = FL_TIMES;
  textsize_     = 12;
  value_        = NULL;

  blocks_.clear();

  link_         = (Fl_Help_Func *)0;

  link_list_.clear();

  directory_.clear();
  filename_.clear();

  topline_      = 0;
  leftline_     = 0;
  size_         = 0;
  hsize_        = 0;
  scrollbar_size_ = 0;

  scrollbar_.value(0, hh, 0, 1);
  scrollbar_.step(8.0);
  scrollbar_.show();
  scrollbar_.callback(scrollbar_callback);

  hscrollbar_.value(0, ww, 0, 1);
  hscrollbar_.step(8.0);
  hscrollbar_.show();
  hscrollbar_.callback(hscrollbar_callback);
  hscrollbar_.type(FL_HORIZONTAL);
  end();

  resize(xx, yy, ww, hh);
}


/** 
  \brief Destroys the Fl_Help_View widget.

  The destructor destroys the widget and frees all memory that has been
  allocated for the current document.
*/
Fl_Help_View::~Fl_Help_View()
{
  clear_selection();
  free_data();
}


/** 
  \brief Return the current filename for the text in the buffer.

  Fl_Help_View remains the owner of the allocated memory. If the filename
  chages, the returned pointer will become stale. 

  \return nullptr if the filename is empty
*/
const char *Fl_Help_View::filename() const { 
  if (filename_.empty()) 
    return nullptr;
  else 
    return filename_.c_str(); 
}


/** 
  \brief Return the current directory for the text in the buffer.

  Fl_Help_View remains the owner of the allocated memory. If the directory
  chages, the returned pointer will become stale. 

  \return nullptr if the directory name is empty
*/
const char *Fl_Help_View::directory() const { 
  if (directory_.empty()) 
    return nullptr;
  else 
    return directory_.c_str(); 
}


/** 
  \brief Return the title of the current document.

  Fl_Help_View remains the owner of the allocated memory. If the document
  chages, the returned pointer will become stale. 

  \return empty string if the directory name is empty
 */
const char *Fl_Help_View::title() const { 
  return title_.c_str();
}


/**
  \brief Set a callback function for following links.

  This method assigns a callback function to use when a link is
  followed or a file is loaded (via Fl_Help_View::load()) that
  requires a different file or path.

  The callback function receives a pointer to the Fl_Help_View
  widget and the URI or full pathname for the file in question.
  It must return a pathname that can be opened as a local file or NULL:

  \code
  const char *fn(Fl_Widget *w, const char *uri);
  \endcode

  The link function can be used to retrieve remote or virtual
  documents, returning a temporary file that contains the actual
  data. If the link function returns NULL, the value of
  the Fl_Help_View widget will remain unchanged.

  If the link callback cannot handle the URI scheme, it should
  return the uri value unchanged or set the value() of the widget
  before returning NULL.

  \param[in] fn Pointer to the callback function
*/
void Fl_Help_View::link(Fl_Help_Func *fn) { 
  link_ = fn; 
}


/** 
  \brief Loads the specified file.

  This method loads the specified file or URL. The filename may end in a
  \c \#name style target.

  If the URL starts with \a ftp, \a http, \a https, \a ipp, \a mailto, or
  \a news, followed by a colon, FLTK will use fl_open_uri() to show the
  requested page in an external browser.

  In all other cases, the URL is interpreted as a filename. The file is read and
  displayed in this browser. Note that Windows style backslashes are not
  supported in the file name.

  \param[in] f filename or URL
  \return 0 on success, -1 on error

  \see fl_open_uri()
*/
int Fl_Help_View::load(const char *f)
{
  FILE          *fp;            // File to read from
  long          len;            // Length of file
  std::string target;        // Target in file
  std::string localname;     // Local filename
  std::string error; // Error buffer
  std::string newname;   // New filename buffer

  // printf("load(%s)\n",f); fflush(stdout);

  if (strncmp(f, "ftp:", 4) == 0 ||
      strncmp(f, "http:", 5) == 0 ||
      strncmp(f, "https:", 6) == 0 ||
      strncmp(f, "ipp:", 4) == 0 ||
      strncmp(f, "mailto:", 7) == 0 ||
      strncmp(f, "news:", 5) == 0)
  {
    char urimsg[FL_PATH_MAX]; // Use of static size ok.
    if ( fl_open_uri(f, urimsg, sizeof(urimsg)) == 0 ) {
      clear_selection();

      newname = f;
      size_t hash_pos = newname.find_last_of('#');
      if (hash_pos != std::string::npos) {
        target = newname.substr(hash_pos + 1);
        newname.resize(hash_pos);
      }

      if (link_) {
        const char *n = (*link_)(this, newname.c_str());
        if (n == nullptr)
          return 0;
        localname = n;
      } else {
        localname = newname;
      }

      free_data();

      filename_ = newname;

      // Note: We do not support Windows backslashes, since they are illegal
      //       in URLs...
      directory_ = newname;
      size_t slash_pos = directory_.find_last_of('/');
      if (slash_pos == std::string::npos) {
        directory_.clear();
      } else if ((slash_pos > 0) && (directory_[slash_pos-1] != '/')) {
        directory_.resize(slash_pos);
      }

      error = "Error"
              "

Error

" "

Unable to follow the link \"" + std::string(f) + "\" - " + std::string(urimsg) + ".

"; value(error.c_str()); return -1; } else { return 0; } } clear_selection(); newname = f; size_t hash_pos = newname.find_last_of('#'); if (hash_pos != std::string::npos) { target = newname.substr(hash_pos + 1); newname.resize(hash_pos); } if (link_) { const char *n = (*link_)(this, newname.c_str()); if (n == nullptr) return -1; localname = n; } else { localname = newname; } free_data(); filename_ = newname; directory_ = newname; // Note: We do not support Windows backslashes, since they are illegal // in URLs... size_t slash_pos = directory_.find_last_of('/'); if (slash_pos == std::string::npos) { directory_.clear(); } else if ((slash_pos > 0) && (directory_[slash_pos-1] != '/')) { directory_.resize(slash_pos); } if (localname.find("file:") == 0) { localname.erase(0, 5); // Adjust for local filename... } int ret = 0; if ((fp = fl_fopen(localname.c_str(), "rb")) != NULL) { fseek(fp, 0, SEEK_END); len = ftell(fp); rewind(fp); value_ = (const char *)calloc(len + 1, 1); if (fread((void *)value_, 1, len, fp)==0) { /* use default 0 */ } fclose(fp); } else { error = "Error" "

Error

" "

Unable to follow the link \"" +localname + "\" - " + strerror(errno) + ".

"; value_ = fl_strdup(error.c_str()); ret = -1; } initial_load = 1; format(); initial_load = 0; if (!target.empty()) topline(target.c_str()); else topline(0); return ret; } /** \brief Override the superclass's resize method. \param[in] xx, yy, ww, hh New position and size of the widget */ void Fl_Help_View::resize(int xx, int yy, int ww, int hh) { Fl_Boxtype b = box() ? box() : FL_DOWN_BOX; // Box to draw... Fl_Widget::resize(xx, yy, ww, hh); int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size(); scrollbar_.resize(x() + w() - scrollsize - Fl::box_dw(b) + Fl::box_dx(b), y() + Fl::box_dy(b), scrollsize, h() - scrollsize - Fl::box_dh(b)); hscrollbar_.resize(x() + Fl::box_dx(b), y() + h() - scrollsize - Fl::box_dh(b) + Fl::box_dy(b), w() - scrollsize - Fl::box_dw(b), scrollsize); format(); } /** \brief Scroll the text to the given anchor. \param[in] anchor scroll to this named anchor */ void Fl_Help_View::topline(const char *anchor) { std::string target_name = to_lower(anchor); // Convert to lower case auto tl = target_line_map_.find(target_name); if (tl != target_line_map_.end()) { // Found the target name, scroll to the line topline(tl->second); } else { // Scroll to the top. topline(0); } } /** \brief Scrolls the text to the indicated position, given a pixel line. If the given pixel value \p top is out of range, then the text is scrolled to the top or bottom of the document, resp. \param[in] top top line number in pixels (0 = start of document) */ void Fl_Help_View::topline(int top) { if (!value_) return; int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size(); if (size_ < (h() - scrollsize) || top < 0) top = 0; else if (top > size_) top = size_; topline_ = top; scrollbar_.value(topline_, h() - scrollsize, 0, size_); do_callback(FL_REASON_DRAGGED); redraw(); } /** \brief Scrolls the text to the indicated position, given a pixel column. If the given pixel value \p left is out of range, then the text is scrolled to the left or right side of the document, resp. \param[in] left left column number in pixels (0 = left side) */ void Fl_Help_View::leftline(int left) { if (!value_) return; int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size(); if (hsize_ < (w() - scrollsize) || left < 0) left = 0; else if (left > hsize_) left = hsize_; leftline_ = left; hscrollbar_.value(leftline_, w() - scrollsize, 0, hsize_); redraw(); } /** \brief Sets the current help text buffer to the string provided and reformats the text. The provided character string \p val is copied internally and will be freed when value() is called again, or when the widget is destroyed. If \p val is nullptr, then the widget is cleared. \param[in] val Text to view, or nullptr to clear the widget, Fl_Help_View will creat a local copy of the string. */ void Fl_Help_View::value(const char *val) { clear_selection(); free_data(); set_changed(); if (!val) return; value_ = fl_strdup(val); initial_load = 1; format(); initial_load = 0; topline(0); leftline(0); } /* \brief Returns the Unicode Code Point associated with a quoted character (aka "HTML Entity"). Possible encoding formats: - `&name;` named entity - `&#nn..;` numeric (decimal) Unicode Code Point - `&#xnn..;` numeric (hexadecimal) Unicode Code Point - `&#Xnn..;` numeric (hexadecimal) Unicode Code Point `nn..` = decimal or hexadecimal number, resp. Contents of the table `names[]` below: All printable ASCII (32-126) and ISO-8859-1 (160-255) characters are encoded with the same value in Unicode. Special characters outside the range [0-255] are encoded with their Unicode Code Point as hexadecimal constants. Example: - Euro sign: (Unicode) U+20ac = (hex) 0x20ac \note Converted to correct Unicode values and tested (compared with the display of Firefox). AlbrechtS, 14 Feb. 2016. \note if you add or remove items to/from this list, please update the documentation for Fl_Help_View::Fl_Help_View(). \param[in] p Pointer to the quoted character string, e.g. `©` or `©` \return the Unicode Code Point for the quoted character, or -1 if not found. */ static int quote_char(const char *p) { int i; static const struct { const char *name; int namelen; int code; } *nameptr, // Pointer into name array names[] = // Quoting names { { "Aacute;", 7, 193 }, { "aacute;", 7, 225 }, { "Acirc;", 6, 194 }, { "acirc;", 6, 226 }, { "acute;", 6, 180 }, { "AElig;", 6, 198 }, { "aelig;", 6, 230 }, { "Agrave;", 7, 192 }, { "agrave;", 7, 224 }, { "amp;", 4, '&' }, { "Aring;", 6, 197 }, { "aring;", 6, 229 }, { "Atilde;", 7, 195 }, { "atilde;", 7, 227 }, { "Auml;", 5, 196 }, { "auml;", 5, 228 }, { "brvbar;", 7, 166 }, { "bull;", 5, 0x2022 }, { "Ccedil;", 7, 199 }, { "ccedil;", 7, 231 }, { "cedil;", 6, 184 }, { "cent;", 5, 162 }, { "copy;", 5, 169 }, { "curren;", 7, 164 }, { "dagger;", 7, 0x2020 }, { "deg;", 4, 176 }, { "divide;", 7, 247 }, { "Eacute;", 7, 201 }, { "eacute;", 7, 233 }, { "Ecirc;", 6, 202 }, { "ecirc;", 6, 234 }, { "Egrave;", 7, 200 }, { "egrave;", 7, 232 }, { "ETH;", 4, 208 }, { "eth;", 4, 240 }, { "Euml;", 5, 203 }, { "euml;", 5, 235 }, { "euro;", 5, 0x20ac }, { "frac12;", 7, 189 }, { "frac14;", 7, 188 }, { "frac34;", 7, 190 }, { "gt;", 3, '>' }, { "Iacute;", 7, 205 }, { "iacute;", 7, 237 }, { "Icirc;", 6, 206 }, { "icirc;", 6, 238 }, { "iexcl;", 6, 161 }, { "Igrave;", 7, 204 }, { "igrave;", 7, 236 }, { "iquest;", 7, 191 }, { "Iuml;", 5, 207 }, { "iuml;", 5, 239 }, { "laquo;", 6, 171 }, { "lt;", 3, '<' }, { "macr;", 5, 175 }, { "micro;", 6, 181 }, { "middot;", 7, 183 }, { "nbsp;", 5, ' ' }, { "ndash;", 6, 0x2013 }, { "not;", 4, 172 }, { "Ntilde;", 7, 209 }, { "ntilde;", 7, 241 }, { "Oacute;", 7, 211 }, { "oacute;", 7, 243 }, { "Ocirc;", 6, 212 }, { "ocirc;", 6, 244 }, { "Ograve;", 7, 210 }, { "ograve;", 7, 242 }, { "ordf;", 5, 170 }, { "ordm;", 5, 186 }, { "Oslash;", 7, 216 }, { "oslash;", 7, 248 }, { "Otilde;", 7, 213 }, { "otilde;", 7, 245 }, { "Ouml;", 5, 214 }, { "ouml;", 5, 246 }, { "para;", 5, 182 }, { "permil;", 7, 0x2030 }, { "plusmn;", 7, 177 }, { "pound;", 6, 163 }, { "quot;", 5, '\"' }, { "raquo;", 6, 187 }, { "reg;", 4, 174 }, { "sect;", 5, 167 }, { "shy;", 4, 173 }, { "sup1;", 5, 185 }, { "sup2;", 5, 178 }, { "sup3;", 5, 179 }, { "szlig;", 6, 223 }, { "THORN;", 6, 222 }, { "thorn;", 6, 254 }, { "times;", 6, 215 }, { "trade;", 6, 0x2122 }, { "Uacute;", 7, 218 }, { "uacute;", 7, 250 }, { "Ucirc;", 6, 219 }, { "ucirc;", 6, 251 }, { "Ugrave;", 7, 217 }, { "ugrave;", 7, 249 }, { "uml;", 4, 168 }, { "Uuml;", 5, 220 }, { "uuml;", 5, 252 }, { "Yacute;", 7, 221 }, { "yacute;", 7, 253 }, { "yen;", 4, 165 }, { "Yuml;", 5, 0x0178 }, { "yuml;", 5, 255 } }; if (!strchr(p, ';')) return -1; if (*p == '#') { if (*(p+1) == 'x' || *(p+1) == 'X') { return (int)strtol(p+2, NULL, 16); } else { return atoi(p+1); } } for (i = (int)(sizeof(names) / sizeof(names[0])), nameptr = names; i > 0; i --, nameptr ++) { if (strncmp(p, nameptr->name, nameptr->namelen) == 0) return nameptr->code; } return -1; } /** \brief Get the current size of the scrollbars' troughs, in pixels. If this value is zero (default), this widget will use the Fl::scrollbar_size() value as the scrollbar's width. \returns Scrollbar size in pixels, or 0 if the global Fl::scrollbar_size() is being used. \see Fl::scrollbar_size(int) */ int Fl_Help_View::scrollbar_size() const { return(scrollbar_size_); } /** \brief Set the pixel size of the scrollbars' troughs to \p newSize, in pixels. Normally you should not need this method, and should use Fl::scrollbar_size(int) instead to manage the size of ALL your widgets' scrollbars. This ensures your application has a consistent UI, is the default behavior, and is normally what you want. Only use THIS method if you really need to override the global scrollbar size. The need for this should be rare. Setting \p newSize to the special value of 0 causes the widget to track the global Fl::scrollbar_size(), which is the default. \param[in] newSize Sets the scrollbar size in pixels.\n If 0 (default), scrollbar size tracks the global Fl::scrollbar_size() \see Fl::scrollbar_size() */ void Fl_Help_View::scrollbar_size(int newSize) { scrollbar_size_ = newSize; } /** \brief The vertical scrollbar callback. */ static void scrollbar_callback(Fl_Widget *s, void *) { ((Fl_Help_View *)(s->parent()))->topline(int(((Fl_Scrollbar*)s)->value())); } /** \brief The horizontal scrollbar callback. */ static void hscrollbar_callback(Fl_Widget *s, void *) { ((Fl_Help_View *)(s->parent()))->leftline(int(((Fl_Scrollbar*)s)->value())); } // Note: The CMP recommends that the Doxygen comment should be placed where // the implementation is. I decided to place the Doxygen comment here, because // the Fl_Help_View class is huge, and the header file should IMHO not be // cluttered with comments. Some implementations stay in the header to make // inlinig easier for the compiler. // - Matt /** \fn int Fl_Help_View::size() const \brief Gets the size of the help view. */ /** \fn void Fl_Help_View::textcolor(Fl_Color c) \brief Sets the default text color. */ /** \fn Fl_Color Fl_Help_View::textcolor() const \brief Returns the current default text color. */ /** \fn void Fl_Help_View::textfont(Fl_Font f) \brief Sets the default text font. */ /** \fn Fl_Font Fl_Help_View::textfont() const \brief Returns the current default text font. */ /** \fn void Fl_Help_View::textsize(Fl_Fontsize s) \brief Sets the default text size. */ /** \fn Fl_Fontsize Fl_Help_View::textsize() const \brief Gets the default text size. */ /** \fn int Fl_Help_View::topline() const \brief Returns the current top line in pixels. */ /** \fn int Fl_Help_View::leftline() \brief Gets the left position in pixels. */ /** \fn const char *Fl_Help_View::value() const \brief Returns the current buffer contents. */