/** * @file * "Format" expression-related functions of the \e GtkExperimentTranscript * widget */ /* * Copyright (C) 2012 Otto-von-Guericke-Universität Magdeburg * * This program 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, either version 3 of the License, or * (at your option) any later version. * * This program 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 . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include "gtk-experiment-transcript.h" #include "gtk-experiment-transcript-private.h" #define FORMAT_REGEX_COMPILE_FLAGS (G_REGEX_CASELESS) #define FORMAT_REGEX_MATCH_FLAGS (0) /** @bug regexp after end of markup is ignored (e.g. "\bich\b") */ static gboolean gtk_experiment_transcript_parse_format(GtkExperimentTranscriptFormat *fmt, const gchar *str, GError **error) { PangoAttrIterator *iter; gchar *pattern, pattern_captures[255], *p; gint capture_count = 0; g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!pango_parse_markup(str, -1, 0, &fmt->attribs, &pattern, NULL, error)) return FALSE; p = pattern_captures; iter = pango_attr_list_get_iterator(fmt->attribs); do { gint start, end; pango_attr_iterator_range(iter, &start, &end); if (end < G_MAXINT) { *p++ = '('; strncpy(p, pattern + start, end - start); p += end - start; *p++ = ')'; capture_count++; } } while (pango_attr_iterator_next(iter)); pango_attr_iterator_destroy(iter); *p = '\0'; g_free(pattern); fmt->regexp = g_regex_new(pattern_captures, FORMAT_REGEX_COMPILE_FLAGS, 0, error); if (fmt->regexp == NULL) { gtk_experiment_transcript_free_format(fmt); fmt->attribs = NULL; return FALSE; } if (g_regex_get_capture_count(fmt->regexp) != capture_count) { g_set_error(error, GTK_EXPERIMENT_TRANSCRIPT_ERROR, GTK_EXPERIMENT_TRANSCRIPT_ERROR_REGEXCAPTURES, "Additional regular expression captures not allowed"); gtk_experiment_transcript_free_format(fmt); fmt->regexp = NULL; fmt->attribs = NULL; return FALSE; } return TRUE; } /** @private */ void gtk_experiment_transcript_apply_format(GtkExperimentTranscriptFormat *fmt, const gchar *text, PangoAttrList *attrib_list) { GMatchInfo *match_info; if (fmt->regexp == NULL || fmt->attribs == NULL) return; g_regex_match(fmt->regexp, text, FORMAT_REGEX_MATCH_FLAGS, &match_info); while (g_match_info_matches(match_info)) { PangoAttrIterator *iter; gint match_num = 1; iter = pango_attr_list_get_iterator(fmt->attribs); do { gint start, end; pango_attr_iterator_range(iter, &start, &end); if (end == G_MAXINT) continue; start = end = -1; g_match_info_fetch_pos(match_info, match_num, &start, &end); if (start >= 0 && end >= 0) { GSList *attribs; attribs = pango_attr_iterator_get_attrs(iter); for (GSList *cur = attribs; cur != NULL; cur = cur->next) { PangoAttribute *attrib; attrib = pango_attribute_copy((PangoAttribute *)cur->data); attrib->start_index = (guint)start; attrib->end_index = (guint)end; pango_attr_list_change(attrib_list, attrib); } g_slist_free(attribs); } match_num++; } while (pango_attr_iterator_next(iter)); pango_attr_iterator_destroy(iter); g_match_info_next(match_info, NULL); } g_match_info_free(match_info); } /** @private */ void gtk_experiment_transcript_free_formats(GSList *formats) { for (GSList *cur = formats; cur != NULL; cur = cur->next) { GtkExperimentTranscriptFormat *fmt = (GtkExperimentTranscriptFormat *)cur->data; gtk_experiment_transcript_free_format(fmt); g_free(fmt); } g_slist_free(formats); } /* * API */ /** * @brief Load a format file to use with the transcript widget * * Loading a format file applies additional formattings (highlighting) to the * transcript's contributions according to the rules specified in the file. * For information about the format file syntax and semantics, refer to the * "Experiment Player" manual. * * The format file is parsed and and compiled to an internal representation. * * @param trans Widget instance * @param filename File name of format file to load (\c NULL or empty string * resets any formattings of a previously loaded file) * @param error GError to set on failure, or \c NULL * @return \c TRUE on success, else \c FALSE */ gboolean gtk_experiment_transcript_load_formats(GtkExperimentTranscript *trans, const gchar *filename, GError **error) { FILE *file; gchar buf[255]; gint cur_line = 0; gboolean res = FALSE; g_return_val_if_fail(error == NULL || *error == NULL, FALSE); gtk_experiment_transcript_free_formats(trans->priv->formats); trans->priv->formats = NULL; if (filename == NULL || !*filename) { res = TRUE; goto redraw; } if ((file = g_fopen(filename, "r")) == NULL) { g_set_error(error, GTK_EXPERIMENT_TRANSCRIPT_ERROR, GTK_EXPERIMENT_TRANSCRIPT_ERROR_FILEOPEN, "Failed to open format file \"%s\":\n%s", filename, g_strerror(errno)); goto redraw; } /** @bug will fail for lines longer than sizeof(buf)-1 */ while (fgets((char *)buf, sizeof(buf), file) != NULL) { GtkExperimentTranscriptFormat *fmt; cur_line++; g_strchug(buf); switch (*buf) { case '#': case '\r': case '\n': case '\0': continue; } /** @todo null-terminate buf at line end to avoid confusing Pango parse errors referring to line 2 */ fmt = g_new(GtkExperimentTranscriptFormat, 1); if (!gtk_experiment_transcript_parse_format(fmt, buf, error)) { g_prefix_error(error, "Error parsing \"%s\" on line %d:\n", filename, cur_line); g_free(fmt); fclose(file); gtk_experiment_transcript_free_formats(trans->priv->formats); trans->priv->formats = NULL; goto redraw; } trans->priv->formats = g_slist_prepend(trans->priv->formats, fmt); } trans->priv->formats = g_slist_reverse(trans->priv->formats); fclose(file); res = TRUE; redraw: gtk_experiment_transcript_text_layer_redraw(trans); return res; } /** * @brief Specify an interactive format string for a transcript widget * * Associates a single format rule with a transcript widget. The formatting * changes are cumulatively applied after any changes introduced by a format * file loaded into the widget. The interactive format rule is independant of * any format file rules, that is it is not reset when format file rules are * reset. * A rule may also be given without Pango markup (plain regular expression), * applying default formattings for that regular expression. Default formattings * may be configured via public instance attributes of the widget. * For information about the format rule syntax and semantics, refer to the * "Experiment Player" manual. * * @sa gtk_experiment_transcript_load_formats * * @param trans Widget instance * @param format_str Format rule string (with or without markup) * @param with_markup Must be \c TRUE if the format_str contains Pango markup, * else \c FALSE * @param error GError to set on failure, or \c NULL * @return \c TRUE on success, else \c FALSE */ gboolean gtk_experiment_transcript_set_interactive_format(GtkExperimentTranscript *trans, const gchar *format_str, gboolean with_markup, GError **error) { GtkExperimentTranscriptFormat *fmt = &trans->priv->interactive_format; gchar *pattern; PangoAttribute *attrib; gboolean res = FALSE; g_return_val_if_fail(error == NULL || *error == NULL, FALSE); gtk_experiment_transcript_free_format(fmt); fmt->regexp = NULL; fmt->attribs = NULL; if (format_str == NULL || !*format_str) { res = TRUE; goto redraw; } if (with_markup) { res = gtk_experiment_transcript_parse_format(fmt, format_str, error); goto redraw; } /* else if (!with_markup) */ fmt->attribs = pango_attr_list_new(); g_warn_if_fail(fmt->attribs != NULL); if (fmt->attribs == NULL) goto redraw; if (trans->interactive_format.default_font != NULL) { attrib = pango_attr_font_desc_new(trans->interactive_format.default_font); attrib->end_index = 1; pango_attr_list_insert(fmt->attribs, attrib); } if (trans->interactive_format.default_text_color != NULL) { GdkColor *color = trans->interactive_format.default_text_color; attrib = pango_attr_foreground_new(color->red, color->green, color->blue); attrib->end_index = 1; pango_attr_list_insert(fmt->attribs, attrib); } if (trans->interactive_format.default_bg_color != NULL) { GdkColor *color = trans->interactive_format.default_bg_color; attrib = pango_attr_background_new(color->red, color->green, color->blue); attrib->end_index = 1; pango_attr_list_insert(fmt->attribs, attrib); } pattern = g_strconcat("(", format_str, ")", NULL); fmt->regexp = g_regex_new(pattern, FORMAT_REGEX_COMPILE_FLAGS, 0, error); g_free(pattern); if (fmt->regexp == NULL) { gtk_experiment_transcript_free_format(fmt); fmt->attribs = NULL; goto redraw; } if (g_regex_get_capture_count(fmt->regexp) != 1) { g_set_error(error, GTK_EXPERIMENT_TRANSCRIPT_ERROR, GTK_EXPERIMENT_TRANSCRIPT_ERROR_REGEXCAPTURES, "Additional regular expression captures not allowed"); gtk_experiment_transcript_free_format(fmt); fmt->regexp = NULL; fmt->attribs = NULL; goto redraw; } res = TRUE; redraw: gtk_experiment_transcript_text_layer_redraw(trans); return res; }