/* * Copyright 2006 James Bursa * Copyright 2006 Adrian Lees * * This file is part of NetSurf, http://www.netsurf-browser.org/ * * NetSurf is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * NetSurf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file * * plain text content handling implementation. */ #include #include #include "utils/errors.h" #include "utils/corestrings.h" #include "utils/http.h" #include "utils/log.h" #include "utils/messages.h" #include "utils/utils.h" #include "utils/utf8.h" #include "utils/nsoption.h" #include "netsurf/content.h" #include "netsurf/keypress.h" #include "netsurf/browser_window.h" #include "netsurf/plotters.h" #include "netsurf/layout.h" #include "content/content_protected.h" #include "content/content_factory.h" #include "content/hlcache.h" #include "content/textsearch.h" #include "content/handlers/css/utils.h" #include "desktop/selection.h" #include "desktop/gui_internal.h" #include "text/textplain.h" struct textplain_line { size_t start; size_t length; }; /** * plain text content */ typedef struct textplain_content { struct content base; lwc_string *encoding; void *inputstream; char *utf8_data; size_t utf8_data_size; size_t utf8_data_allocated; unsigned long physical_line_count; struct textplain_line *physical_line; int formatted_width; struct browser_window *bw; struct selection *sel; /** Selection state */ } textplain_content; #define CHUNK 32768 /* Must be a power of 2 */ #define MARGIN 4 #define TAB_WIDTH 8 /* must be power of 2 currently */ #define TEXT_SIZE 10 * PLOT_STYLE_SCALE /* Unscaled text size in pt */ static plot_font_style_t textplain_style = { .family = PLOT_FONT_FAMILY_MONOSPACE, .size = TEXT_SIZE, .weight = 400, .flags = FONTF_NONE, .background = 0xffffff, .foreground = 0x000000, }; static int textplain_tab_width = 256; /* try for a sensible default */ static lwc_string *textplain_default_charset; /** * Clean up after the text content handler */ static void textplain_fini(void) { if (textplain_default_charset != NULL) { lwc_string_unref(textplain_default_charset); textplain_default_charset = NULL; } } /** * Work around feature in libparserutils * * if the client provides an encoding up front, but does not provide a * charset detection callback, then libparserutils will replace the * provided encoding with UTF-8. This breaks our input handling. * * Avoid this by providing a callback that does precisely nothing, * thus preserving whatever charset information we decided on in * textplain_create. */ static parserutils_error textplain_charset_hack(const uint8_t *data, size_t len, uint16_t *mibenum, uint32_t *source) { return PARSERUTILS_OK; } /** * setup plain text render. * * \param[in] c content object. * \param[in] encoding the encoding of the content. * \return NSERROR_OK else appropriate error code. */ static nserror textplain_create_internal(textplain_content *c, lwc_string *encoding) { char *utf8_data; parserutils_inputstream *stream; parserutils_error error; textplain_style.size = (nsoption_int(font_size) * PLOT_STYLE_SCALE) / 10; utf8_data = malloc(CHUNK); if (utf8_data == NULL) goto no_memory; error = parserutils_inputstream_create(lwc_string_data(encoding), 0, textplain_charset_hack, &stream); if (error == PARSERUTILS_BADENCODING) { /* Fall back to Windows-1252 */ error = parserutils_inputstream_create("Windows-1252", 0, textplain_charset_hack, &stream); } if (error != PARSERUTILS_OK) { free(utf8_data); goto no_memory; } c->encoding = lwc_string_ref(encoding); c->inputstream = stream; c->utf8_data = utf8_data; c->utf8_data_size = 0; c->utf8_data_allocated = CHUNK; c->physical_line = 0; c->physical_line_count = 0; c->formatted_width = 0; c->bw = NULL; c->sel = selection_create((struct content *)c); return NSERROR_OK; no_memory: content_broadcast_error(&c->base, NSERROR_NOMEM, NULL); return NSERROR_NOMEM; } /** * Create a CONTENT_TEXTPLAIN. */ static nserror textplain_create(const content_handler *handler, lwc_string *imime_type, const struct http_parameter *params, llcache_handle *llcache, const char *fallback_charset, bool quirks, struct content **c) { textplain_content *text; nserror error; lwc_string *encoding; text = calloc(1, sizeof(textplain_content)); if (text == NULL) { return NSERROR_NOMEM; } error = content__init(&text->base, handler, imime_type, params, llcache, fallback_charset, quirks); if (error != NSERROR_OK) { free(text); return error; } error = http_parameter_list_find_item(params, corestring_lwc_charset, &encoding); if (error != NSERROR_OK) { encoding = lwc_string_ref(textplain_default_charset); } error = textplain_create_internal(text, encoding); if (error != NSERROR_OK) { lwc_string_unref(encoding); free(text); return error; } lwc_string_unref(encoding); *c = (struct content *) text; return NSERROR_OK; } /** * copy utf8 encoded data */ static bool textplain_copy_utf8_data(textplain_content *c, const uint8_t *buf, size_t len) { if (c->utf8_data_size + len >= c->utf8_data_allocated) { /* Compute next multiple of chunk above the required space */ size_t allocated; char *utf8_data; allocated = (c->utf8_data_size + len + CHUNK - 1) & ~(CHUNK - 1); utf8_data = realloc(c->utf8_data, allocated); if (utf8_data == NULL) return false; c->utf8_data = utf8_data; c->utf8_data_allocated = allocated; } memcpy(c->utf8_data + c->utf8_data_size, buf, len); c->utf8_data_size += len; return true; } /** * drain input */ static bool textplain_drain_input(textplain_content *c, parserutils_inputstream *stream, parserutils_error terminator) { static const uint8_t *u_fffd = (const uint8_t *) "\xef\xbf\xbd"; const uint8_t *ch; size_t chlen, offset = 0; while (parserutils_inputstream_peek(stream, offset, &ch, &chlen) != terminator) { /* Replace all instances of NUL with U+FFFD */ if (chlen == 1 && *ch == 0) { if (offset > 0) { /* Obtain pointer to start of input data */ parserutils_inputstream_peek(stream, 0, &ch, &chlen); /* Copy from it up to the start of the NUL */ if (textplain_copy_utf8_data(c, ch, offset) == false) return false; } /* Emit U+FFFD */ if (textplain_copy_utf8_data(c, u_fffd, 3) == false) return false; /* Advance inputstream past the NUL we just read */ parserutils_inputstream_advance(stream, offset + 1); /* Reset the read offset */ offset = 0; } else { /* Accumulate input */ offset += chlen; if (offset > CHUNK) { /* Obtain pointer to start of input data */ parserutils_inputstream_peek(stream, 0, &ch, &chlen); /* Emit the data we've read */ if (textplain_copy_utf8_data(c, ch, offset) == false) return false; /* Advance the inputstream */ parserutils_inputstream_advance(stream, offset); /* Reset the read offset */ offset = 0; } } } if (offset > 0) { /* Obtain pointer to start of input data */ parserutils_inputstream_peek(stream, 0, &ch, &chlen); /* Emit any data remaining */ if (textplain_copy_utf8_data(c, ch, offset) == false) return false; /* Advance the inputstream past the data we've read */ parserutils_inputstream_advance(stream, offset); } return true; } /** * Process data for CONTENT_TEXTPLAIN. */ static bool textplain_process_data(struct content *c, const char *data, unsigned int size) { textplain_content *text = (textplain_content *) c; parserutils_inputstream *stream = text->inputstream; parserutils_error error; error = parserutils_inputstream_append(stream, (const uint8_t *) data, size); if (error != PARSERUTILS_OK) { goto no_memory; } if (textplain_drain_input(text, stream, PARSERUTILS_NEEDDATA) == false) goto no_memory; return true; no_memory: content_broadcast_error(c, NSERROR_NOMEM, NULL); return false; } /** * Convert a CONTENT_TEXTPLAIN for display. */ static bool textplain_convert(struct content *c) { textplain_content *text = (textplain_content *) c; parserutils_inputstream *stream = text->inputstream; parserutils_error error; error = parserutils_inputstream_append(stream, NULL, 0); if (error != PARSERUTILS_OK) { return false; } if (textplain_drain_input(text, stream, PARSERUTILS_EOF) == false) return false; parserutils_inputstream_destroy(stream); text->inputstream = NULL; content_set_ready(c); content_set_done(c); content_set_status(c, messages_get("Done")); return true; } /** * Calculate the line height, in pixels * * \return Line height, in pixels */ static float textplain_line_height(void) { /* Size is in points, so convert to pixels. * Then use a constant line height of 1.2 x font size. */ return FIXTOFLT(FDIV((FMUL(FLTTOFIX(1.2), FMUL(nscss_screen_dpi, INTTOFIX((textplain_style.size / PLOT_STYLE_SCALE))))), F_72)); } /** * Reformat a CONTENT_TEXTPLAIN to a new width. */ static void textplain_reformat(struct content *c, int width, int height) { textplain_content *text = (textplain_content *) c; char *utf8_data = text->utf8_data; size_t utf8_data_size = text->utf8_data_size; unsigned long line_count = 0; struct textplain_line *line = text->physical_line; struct textplain_line *line1; size_t i, space, col; size_t columns = 80; int character_width; size_t line_start; nserror res; NSLOG(netsurf, INFO, "content %p w:%d h:%d", c, width, height); /* compute available columns (assuming monospaced font) - use 8 * characters for better accuracy */ res = guit->layout->width(&textplain_style, "ABCDEFGH", 8, &character_width); if (res != NSERROR_OK) { return; } columns = (width - MARGIN - MARGIN) * 8 / character_width; textplain_tab_width = (TAB_WIDTH * character_width) / 8; text->formatted_width = width; text->physical_line_count = 0; if (!line) { text->physical_line = line = malloc(sizeof(struct textplain_line) * (1024 + 3)); if (!line) goto no_memory; } line[line_count++].start = line_start = 0; space = 0; i = 0; col = 0; while (i < utf8_data_size) { size_t csize; /* number of bytes in character */ uint32_t chr; bool term; size_t next_col; parserutils_error perror; perror = parserutils_charset_utf8_to_ucs4((const uint8_t *)utf8_data + i, utf8_data_size - i, &chr, &csize); if (perror != PARSERUTILS_OK) { chr = 0xfffd; } term = (chr == '\n' || chr == '\r'); next_col = col + 1; if (chr == '\t') { next_col = (next_col + TAB_WIDTH - 1) & ~(TAB_WIDTH - 1); } if (term || next_col >= columns) { if (line_count % 1024 == 0) { line1 = realloc(line, sizeof(struct textplain_line) * (line_count + 1024 + 3)); if (!line1) goto no_memory; text->physical_line = line = line1; } if (term) { line[line_count-1].length = i - line_start; /* skip second char of CR/LF or LF/CR pair */ if (i + 1 < utf8_data_size && utf8_data[i+1] != utf8_data[i] && (utf8_data[i+1] == '\n' || utf8_data[i+1] == '\r')) { i++; } } else { if (space) { /* break at last space in line */ i = space; line[line_count-1].length = (i + 1) - line_start; } else line[line_count-1].length = i - line_start; } line[line_count++].start = line_start = i + 1; col = 0; space = 0; } else { col++; if (chr == ' ') space = i; } i += csize; } line[line_count-1].length = i - line[line_count-1].start; line[line_count].start = utf8_data_size; text->physical_line_count = line_count; c->width = width; c->height = line_count * textplain_line_height() + MARGIN + MARGIN; return; no_memory: NSLOG(netsurf, INFO, "out of memory (line_count %lu)", line_count); return; } /** * Destroy a CONTENT_TEXTPLAIN and free all resources it owns. */ static void textplain_destroy(struct content *c) { textplain_content *text = (textplain_content *) c; lwc_string_unref(text->encoding); if (text->inputstream != NULL) { parserutils_inputstream_destroy(text->inputstream); } if (text->physical_line != NULL) { free(text->physical_line); } if (text->utf8_data != NULL) { free(text->utf8_data); } if (text->sel != NULL) { selection_destroy(text->sel); } } static nserror textplain_clone(const struct content *old, struct content **newc) { const textplain_content *old_text = (textplain_content *) old; textplain_content *text; nserror error; const uint8_t *data; size_t size; text = calloc(1, sizeof(textplain_content)); if (text == NULL) return NSERROR_NOMEM; error = content__clone(old, &text->base); if (error != NSERROR_OK) { content_destroy(&text->base); return error; } /* Simply replay create/process/convert */ error = textplain_create_internal(text, old_text->encoding); if (error != NSERROR_OK) { content_destroy(&text->base); return error; } data = content__get_source_data(&text->base, &size); if (size > 0) { if (textplain_process_data(&text->base, (const char *)data, size) == false) { content_destroy(&text->base); return NSERROR_NOMEM; } } if (old->status == CONTENT_STATUS_READY || old->status == CONTENT_STATUS_DONE) { if (textplain_convert(&text->base) == false) { content_destroy(&text->base); return NSERROR_CLONE_FAILED; } } return NSERROR_OK; } static content_type textplain_content_type(void) { return CONTENT_TEXTPLAIN; } /** * Return byte offset within UTF8 textplain content. * * given the co-ordinates of a point within a textplain content. 'dir' * specifies the direction in which to search (-1 = above-left, +1 = * below-right) if the co-ordinates are not contained within a line. * * \param[in] c content of type CONTENT_TEXTPLAIN * \param[in] x x ordinate of point * \param[in] y y ordinate of point * \param[in] dir direction of search if not within line * \return byte offset of character containing (or nearest to) point */ static size_t textplain_offset_from_coords(struct content *c, int x, int y, int dir) { textplain_content *textc = (textplain_content *) c; float line_height = textplain_line_height(); struct textplain_line *line; const char *text; unsigned nlines; size_t length; int idx; assert(c != NULL); y = (int)((float)(y - MARGIN) / line_height); x -= MARGIN; nlines = textc->physical_line_count; if (!nlines) return 0; if (y <= 0) y = 0; else if ((unsigned)y >= nlines) y = nlines - 1; line = &textc->physical_line[y]; text = textc->utf8_data + line->start; length = line->length; idx = 0; while (x > 0) { size_t next_offset = 0; int width = INT_MAX; while (next_offset < length && text[next_offset] != '\t') { next_offset = utf8_next(text, length, next_offset); } if (next_offset < length) { guit->layout->width(&textplain_style, text, next_offset, &width); } if (x <= width) { int pixel_offset; size_t char_offset; guit->layout->position(&textplain_style, text, next_offset, x, &char_offset, &pixel_offset); idx += char_offset; break; } x -= width; length -= next_offset; text += next_offset; idx += next_offset; /* check if it's within the tab */ width = textplain_tab_width - (width % textplain_tab_width); if (x <= width) break; x -= width; length--; text++; idx++; } return line->start + idx; } /** * Handle mouse clicks and movements in a TEXTPLAIN content window. * * \param c content of type textplain * \param bw browser window * \param mouse mouse state on action * \param x coordinate of mouse * \param y coordinate of mouse */ static nserror textplain_mouse_action(struct content *c, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { textplain_content *text = (textplain_content *) c; browser_pointer_shape pointer = BROWSER_POINTER_DEFAULT; union content_msg_data msg_data; const char *status = 0; size_t idx; int dir = 0; browser_window_set_drag_type(bw, DRAGGING_NONE, NULL); idx = textplain_offset_from_coords(c, x, y, dir); if (selection_click(text->sel, text->bw, mouse, idx)) { if (selection_dragging(text->sel)) { browser_window_set_drag_type(bw, DRAGGING_SELECTION, NULL); status = messages_get("Selecting"); } } else { if (mouse & (BROWSER_MOUSE_DRAG_1 | BROWSER_MOUSE_DRAG_2)) { browser_window_page_drag_start(bw, x, y); pointer = BROWSER_POINTER_MOVE; } } msg_data.explicit_status_text = status; content_broadcast(c, CONTENT_MSG_STATUS, &msg_data); msg_data.pointer = pointer; content_broadcast(c, CONTENT_MSG_POINTER, &msg_data); return NSERROR_OK; } /** * Handle mouse tracking (including drags) in a TEXTPLAIN content window. * * \param c content of type textplain * \param bw browser window * \param mouse state of mouse buttons and modifier keys * \param x coordinate of mouse * \param y coordinate of mouse */ static nserror textplain_mouse_track(struct content *c, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { textplain_content *text = (textplain_content *) c; if (browser_window_get_drag_type(bw) == DRAGGING_SELECTION && !mouse) { int dir = -1; size_t idx; if (selection_dragging_start(text->sel)) dir = 1; idx = textplain_offset_from_coords(c, x, y, dir); selection_track(text->sel, mouse, idx); browser_window_set_drag_type(bw, DRAGGING_NONE, NULL); } switch (browser_window_get_drag_type(bw)) { case DRAGGING_SELECTION: { int dir = -1; size_t idx; if (selection_dragging_start(text->sel)) dir = 1; idx = textplain_offset_from_coords(c, x, y, dir); selection_track(text->sel, mouse, idx); } break; default: textplain_mouse_action(c, bw, mouse, x, y); break; } return NSERROR_OK; } /** * Handle keypresses. * * \param c content of type CONTENT_TEXTPLAIN * \param key The UCS4 character codepoint * \return true if key handled, false otherwise */ static bool textplain_keypress(struct content *c, uint32_t key) { textplain_content *text = (textplain_content *) c; struct selection *sel = text->sel; switch (key) { case NS_KEY_COPY_SELECTION: selection_copy_to_clipboard(sel); return true; case NS_KEY_CLEAR_SELECTION: selection_clear(sel, true); return true; case NS_KEY_SELECT_ALL: selection_select_all(sel); return true; case NS_KEY_ESCAPE: /* if there's no selection, leave Escape for the caller */ return selection_clear(sel, true); } return false; } /** * Redraw a text string with highlighting * (for selection/search) * * \param utf8_text pointer to UTF-8 text string * \param utf8_len length of string, in bytes * \param offset byte offset within textual representation * \param x x ordinate at which to plot text * \param y y ordinate at which to plot text * \param clip pointer to current clip rectangle * \param height height of text string * \param scale current display scale (1.0 = 100%) * \param text Content being redrawn. * \param sel Selection context * \param search Search context * \param ctx current redraw context * \return true iff successful and redraw should proceed */ static bool text_draw(const char *utf8_text, size_t utf8_len, size_t offset, int x, int y, const struct rect *clip, int height, float scale, textplain_content *text, const struct selection *sel, const struct redraw_context *ctx) { bool highlighted = false; plot_font_style_t plot_fstyle; nserror res; /* Need scaled text size to pass to plotters */ plot_fstyle = textplain_style; plot_fstyle.size *= scale; /* is this box part of a selection? */ if (ctx->interactive == true) { unsigned len = utf8_len; unsigned start_idx; unsigned end_idx; /* first try the browser window's current selection */ if (selection_highlighted(sel, offset, offset + len, &start_idx, &end_idx)) { highlighted = true; } /* what about the current search operation, if any? */ if (!highlighted && (text->base.textsearch.context != NULL) && content_textsearch_ishighlighted(text->base.textsearch.context, offset, offset + len, &start_idx, &end_idx)) { highlighted = true; } /* \todo make search terms visible within selected text */ if (highlighted) { struct rect r; unsigned endtxt_idx = end_idx; bool clip_changed = false; bool text_visible = true; int startx, endx; plot_style_t pstyle_fill_hback = *plot_style_fill_white; plot_font_style_t fstyle_hback = plot_fstyle; if (end_idx > utf8_len) { /* adjust for trailing space, not present in * utf8_text */ assert(end_idx == utf8_len + 1); endtxt_idx = utf8_len; } res = guit->layout->width(&textplain_style, utf8_text, start_idx, &startx); if (res != NSERROR_OK) { startx = 0; } res = guit->layout->width(&textplain_style, utf8_text, endtxt_idx, &endx); if (res != NSERROR_OK) { endx = 0; } if (scale != 1.0) { startx *= scale; endx *= scale; } /* draw any text preceding highlighted portion */ if (start_idx > 0) { res = ctx->plot->text(ctx, &plot_fstyle, x, y + (int)(height * 0.75 * scale), utf8_text, start_idx); if (res != NSERROR_OK) { return false; } } pstyle_fill_hback.fill_colour = textplain_style.foreground; /* highlighted portion */ r.x0 = x + startx; r.y0 = y; r.x1 = x + endx; r.y1 = y + height * scale; res = ctx->plot->rectangle(ctx, &pstyle_fill_hback, &r); if (res != NSERROR_OK) { return false; } if (start_idx > 0) { int px0 = max(x + startx, clip->x0); int px1 = min(x + endx, clip->x1); if (px0 < px1) { r.x0 = px0; r.y0 = clip->y0; r.x1 = px1; r.y1 = clip->y1; res = ctx->plot->clip(ctx, &r); if (res != NSERROR_OK) { return false; } clip_changed = true; } else { text_visible = false; } } fstyle_hback.background = pstyle_fill_hback.fill_colour; fstyle_hback.foreground = colour_to_bw_furthest( pstyle_fill_hback.fill_colour); if (text_visible && (ctx->plot->text(ctx, &fstyle_hback, x, y + (int)(height * 0.75 * scale), utf8_text, endtxt_idx) != NSERROR_OK)) { return false; } /* draw any text succeeding highlighted portion */ if (endtxt_idx < utf8_len) { int px0 = max(x + endx, clip->x0); if (px0 < clip->x1) { r.x0 = px0; r.y0 = clip->y0; r.x1 = clip->x1; r.y1 = clip->y1; res = ctx->plot->clip(ctx, &r); if (res != NSERROR_OK) { return false; } clip_changed = true; res = ctx->plot->text(ctx, &plot_fstyle, x, y + (int)(height * 0.75 * scale), utf8_text, utf8_len); if (res != NSERROR_OK) { return false; } } } if (clip_changed && (ctx->plot->clip(ctx, clip) != NSERROR_OK)) { return false; } } } if (!highlighted) { res = ctx->plot->text(ctx, &plot_fstyle, x, y + (int) (height * 0.75 * scale), utf8_text, utf8_len); if (res != NSERROR_OK) { return false; } } return true; } /** * Draw a CONTENT_TEXTPLAIN using the current set of plotters (plot). * * x, y, clip_[xy][01] are in target coordinates. * * \param c content of type CONTENT_TEXTPLAIN * \param data redraw data for this content redraw * \param clip current clip region * \param ctx current redraw context * \return true if successful, false otherwise */ static bool textplain_redraw(struct content *c, struct content_redraw_data *data, const struct rect *clip, const struct redraw_context *ctx) { textplain_content *text = (textplain_content *) c; struct browser_window *bw = text->bw; char *utf8_data = text->utf8_data; long lineno; int x = data->x; int y = data->y; unsigned long line_count = text->physical_line_count; float line_height = textplain_line_height(); float scaled_line_height = line_height * data->scale; long line0 = (clip->y0 - y * data->scale) / scaled_line_height - 1; long line1 = (clip->y1 - y * data->scale) / scaled_line_height + 1; struct textplain_line *line = text->physical_line; size_t length; plot_style_t *plot_style_highlight; nserror res; if (line0 < 0) line0 = 0; if (line1 < 0) line1 = 0; if (line_count < (unsigned long) line0) line0 = line_count; if (line_count < (unsigned long) line1) line1 = line_count; if (line1 < line0) line1 = line0; res = ctx->plot->rectangle(ctx, plot_style_fill_white, clip); if (res != NSERROR_OK) { return false; } if (!line) return true; /* choose a suitable background colour for any highlighted text */ if ((data->background_colour & 0x808080) == 0x808080) plot_style_highlight = plot_style_fill_black; else plot_style_highlight = plot_style_fill_white; /* Set up font plot style */ textplain_style.background = data->background_colour; x = (x + MARGIN) * data->scale; y = (y + MARGIN) * data->scale; for (lineno = line0; lineno != line1; lineno++) { const char *text_d = utf8_data + line[lineno].start; int tab_width = textplain_tab_width * data->scale; size_t offset = 0; int tx = x; if (!tab_width) tab_width = 1; length = line[lineno].length; if (!length) continue; while (offset < length) { size_t next_offset = offset; int width; int ntx; nserror res; while ((next_offset < length) && (text_d[next_offset] != '\t')) { next_offset = utf8_next(text_d, length, next_offset); } if (!text_draw(text_d + offset, next_offset - offset, line[lineno].start + offset, tx, y + (lineno * scaled_line_height), clip, line_height, data->scale, text, text->sel, ctx)) { return false; } if (next_offset >= length) break; res = guit->layout->width(&textplain_style, &text_d[offset], next_offset - offset, &width); /* locate end of string and align to next tab position */ if (res == NSERROR_OK) { tx += (int)(width * data->scale); } ntx = x + ((1 + (tx - x) / tab_width) * tab_width); /* if the tab character lies within the * selection, if any, then we must draw it as * a filled rectangle so that it's consistent * with background of the selected text */ if (bw) { unsigned tab_ofst = line[lineno].start + next_offset; struct selection *sel = text->sel; bool highlighted = false; unsigned start_idx, end_idx; if (selection_highlighted(sel, tab_ofst, tab_ofst + 1, &start_idx, &end_idx)) { highlighted = true; } if (!highlighted && (c->textsearch.context != NULL)) { unsigned start_idx, end_idx; if (content_textsearch_ishighlighted( c->textsearch.context, tab_ofst, tab_ofst + 1, &start_idx, &end_idx)) { highlighted = true; } } if (highlighted) { struct rect rect; rect.x0 = tx; rect.y0 = y + (lineno * scaled_line_height); rect.x1 = ntx; rect.y1 = rect.y0 + scaled_line_height; res = ctx->plot->rectangle(ctx, plot_style_highlight, &rect); if (res != NSERROR_OK) { return false; } } } offset = next_offset + 1; tx = ntx; } } return true; } /** * Handle a window containing a CONTENT_TEXTPLAIN being opened. */ static nserror textplain_open(struct content *c, struct browser_window *bw, struct content *page, struct object_params *params) { textplain_content *text = (textplain_content *) c; text->bw = bw; /* text selection */ selection_init(text->sel); return NSERROR_OK; } /** * Handle a window containing a CONTENT_TEXTPLAIN being closed. */ static nserror textplain_close(struct content *c) { textplain_content *text = (textplain_content *) c; text->bw = NULL; return NSERROR_OK; } /** * Return an textplain content's selection context */ static char *textplain_get_selection(struct content *c) { textplain_content *text = (textplain_content *) c; return selection_get_copy(text->sel); } /** * Convert a character offset within a line of text into the * horizontal co-ordinate * * The conversion takes into account the font being used and any tabs * in the text * * \param text line of text * \param offset char offset within text * \param length line length * \return x ordinate */ static int textplain_coord_from_offset(const char *text, size_t offset, size_t length) { int x = 0; while (offset > 0) { size_t next_offset = 0; int tx; while (next_offset < offset && text[next_offset] != '\t') { next_offset = utf8_next(text, length, next_offset); } guit->layout->width(&textplain_style, text, next_offset, &tx); x += tx; if (next_offset >= offset) break; /* align to next tab boundary */ next_offset++; x = (1 + (x / textplain_tab_width)) * textplain_tab_width; offset -= next_offset; text += next_offset; length -= next_offset; } return x; } /** * Retrieve number of lines in content * * \param[in] c Content to retrieve line count from * \return Number of lines */ static unsigned long textplain_line_count(struct content *c) { textplain_content *text = (textplain_content *) c; assert(c != NULL); return text->physical_line_count; } /** * Return a pointer to the requested line of text. * * \param[in] c content of type CONTENT_TEXTPLAIN * \param[in] lineno line number * \param[out] poffset receives byte offset of line start within text * \param[out] plen receives length of returned line * \return pointer to text, or NULL if invalid line number */ static char * textplain_get_line(struct content *c, unsigned lineno, size_t *poffset, size_t *plen) { textplain_content *text = (textplain_content *) c; struct textplain_line *line; assert(c != NULL); if (lineno >= text->physical_line_count) return NULL; line = &text->physical_line[lineno]; *poffset = line->start; *plen = line->length; return text->utf8_data + line->start; } /** * Find line number of byte in text * * Given a byte offset within the text, return the line number * of the line containing that offset. * * \param[in] c content of type CONTENT_TEXTPLAIN * \param[in] offset byte offset within textual representation * \return line number, or -1 if offset invalid (larger than size) */ static int textplain_find_line(struct content *c, unsigned offset) { textplain_content *text = (textplain_content *) c; struct textplain_line *line; int nlines; int lineno = 0; assert(c != NULL); line = text->physical_line; nlines = text->physical_line_count; if (offset > text->utf8_data_size) { return -1; } /* \todo - implement binary search here */ while (lineno < nlines && line[lineno].start < offset) { lineno++; } if (line[lineno].start > offset) { lineno--; } return lineno; } /** * Finds all occurrences of a given string in a textplain content * * \param c the content to be searched * \param context The search context to add the entry to. * \param pattern the string pattern to search for * \param p_len pattern length * \param case_sens whether to perform a case sensitive search * \return NSERROR_OK on success else error code on faliure */ static nserror textplain_textsearch_find(struct content *c, struct textsearch_context *context, const char *pattern, int p_len, bool case_sens) { int nlines = textplain_line_count(c); int line; nserror res = NSERROR_OK; for(line = 0; line < nlines; line++) { size_t offset, length; const char *text; text = textplain_get_line(c, line, &offset, &length); if (text) { while (length > 0) { unsigned match_length; size_t start_idx; const char *new_text; const char *pos; pos = content_textsearch_find_pattern( text, length, pattern, p_len, case_sens, &match_length); if (!pos) break; /* found string in line => add to list */ start_idx = offset + (pos - text); res = content_textsearch_add_match(context, start_idx, start_idx + match_length, NULL, NULL); if (res != NSERROR_OK) { return res; } new_text = pos + match_length; offset += (new_text - text); length -= (new_text - text); text = new_text; } } } return res; } /** * Given a range of byte offsets within a UTF8 textplain content, * return a box that fully encloses the text * * \param[in] c content of type CONTENT_TEXTPLAIN * \param[in] start byte offset of start of text range * \param[in] end byte offset of end * \param[out] r rectangle to be completed */ static void textplain_coords_from_range(struct content *c, unsigned start, unsigned end, struct rect *r) { textplain_content *text = (textplain_content *) c; float line_height = textplain_line_height(); char *utf8_data; struct textplain_line *line; unsigned lineno = 0; unsigned nlines; assert(c != NULL); assert(start <= end); assert(end <= text->utf8_data_size); utf8_data = text->utf8_data; nlines = text->physical_line_count; line = text->physical_line; /* find start */ lineno = textplain_find_line(c, start); r->y0 = (int)(MARGIN + lineno * line_height); if (lineno + 1 <= nlines || line[lineno + 1].start >= end) { /* \todo - it may actually be more efficient just to * run forwards most of the time */ /* find end */ lineno = textplain_find_line(c, end); r->x0 = 0; r->x1 = text->formatted_width; } else { /* single line */ const char *text = utf8_data + line[lineno].start; r->x0 = textplain_coord_from_offset(text, start - line[lineno].start, line[lineno].length); r->x1 = textplain_coord_from_offset(text, end - line[lineno].start, line[lineno].length); } r->y1 = (int)(MARGIN + (lineno + 1) * line_height); } /** * Return a pointer to the raw UTF-8 data, as opposed to the reformatted * text to fit the window width. Thus only hard newlines are preserved * in the saved/copied text of a selection. * * \param[in] c content of type CONTENT_TEXTPLAIN * \param[in] start starting byte offset within UTF-8 text * \param[in] end ending byte offset * \param[out] plen receives validated length * \return pointer to text, or NULL if no text */ static char * textplain_get_raw_data(struct content *c, unsigned start, unsigned end, size_t *plen) { textplain_content *text = (textplain_content *) c; size_t utf8_size; assert(c != NULL); utf8_size = text->utf8_data_size; /* any text at all? */ if (!utf8_size) return NULL; /* clamp to valid offset range */ if (start >= utf8_size) start = utf8_size; if (end >= utf8_size) end = utf8_size; *plen = end - start; return text->utf8_data + start; } /** * get bounds of a free text search match */ static nserror textplain_textsearch_bounds(struct content *c, unsigned start_idx, unsigned end_idx, struct box *start_box, struct box *end_box, struct rect *bounds) { textplain_coords_from_range(c, start_idx, end_idx, bounds); return NSERROR_OK; } /** * invalidate a region based on offsets into the text cauing a redraw */ static nserror textplain_textselection_redraw(struct content *c, unsigned start_idx, unsigned end_idx) { struct rect r; if (end_idx <= start_idx) { return NSERROR_BAD_PARAMETER; } textplain_coords_from_range(c, start_idx, end_idx, &r); content__request_redraw(c, r.x0, r.y0, r.x1 - r.x0, r.y1 - r.y0); return NSERROR_OK; } static nserror textplain_textselection_copy(struct content *c, unsigned start_idx, unsigned end_idx, struct selection_string *selstr) { const char *text; size_t length; bool res = false; text = textplain_get_raw_data(c, start_idx, end_idx, &length); if (text != NULL) { res = selection_string_append(text, length, false, NULL, selstr); } if (res == false) { return NSERROR_NOMEM; } return NSERROR_OK; } /** * Retrieve the index of the end of the text * * \param[in] c Content to retrieve size of * \return Size, in bytes, of data */ static nserror textplain_textselection_get_end(struct content *c, unsigned *end_idx) { textplain_content *text = (textplain_content *)c; *end_idx = text->utf8_data_size; return NSERROR_OK; } /** * plain text content handler table */ static const content_handler textplain_content_handler = { .fini = textplain_fini, .create = textplain_create, .process_data = textplain_process_data, .data_complete = textplain_convert, .reformat = textplain_reformat, .destroy = textplain_destroy, .mouse_track = textplain_mouse_track, .mouse_action = textplain_mouse_action, .keypress = textplain_keypress, .redraw = textplain_redraw, .open = textplain_open, .close = textplain_close, .get_selection = textplain_get_selection, .clone = textplain_clone, .type = textplain_content_type, .textsearch_find = textplain_textsearch_find, .textsearch_bounds = textplain_textsearch_bounds, .textselection_redraw = textplain_textselection_redraw, .textselection_copy = textplain_textselection_copy, .textselection_get_end = textplain_textselection_get_end, .no_share = true, }; /* exported interface documented in html/textplain.h */ nserror textplain_init(void) { lwc_error lerror; nserror error; lerror = lwc_intern_string("Windows-1252", SLEN("Windows-1252"), &textplain_default_charset); if (lerror != lwc_error_ok) { return NSERROR_NOMEM; } error = content_factory_register_handler("text/plain", &textplain_content_handler); if (error != NSERROR_OK) { lwc_string_unref(textplain_default_charset); } error = content_factory_register_handler("application/json", &textplain_content_handler); if (error != NSERROR_OK) { lwc_string_unref(textplain_default_charset); } return error; }