From e9cdd65eeee12ae23bb0a3c103d72e0ff746459b Mon Sep 17 00:00:00 2001
From: Charles Ferguson <gerph@gerph.org>
Date: Sat, 16 Mar 2019 16:36:39 +0000
Subject: [PATCH] Add color name definition to the nanorc configuration.

Rationale:

Syntax definitions include color definitions for the highlights they
perform. Each matching color definition in the syntax must currently
specify a color pair which will be used for the sequences that match.
This means that either users accept the default syntax coloring, or
they copy the syntax definition and change the colors themselves.

This is a tedious and frustrating process for users, and whilst it is
a one-time activity, it does result in configurations that become
static whilst the system provided definitions may change and be
improved.

Change interface:

This change introduces a new command `defcolor` which takes a name,
and a color specification. The names can be used in place of the
fgcolor,bgcolor specifications in the `color` and `icolor` commands,
and will be replaced with the defined colors. This is done repeatedly
so that the color specifications may refer to other color names.

For example, given a C syntax definition like this:

  syntax "c" "*.c"
  color c.comment "//.*"
  defcolor c.comment comment

Together with a standard definition of colors such as:

  defcolor comment green

The default coloring of the C comments would then be green. Should the
user wish to override this color, they would merely update their user
nanorc file with:

  defcolor comment brightblue

If they wanted to /only/ change the C comments, leaving other syntaxes
alone, they could use:

  defcolor c.comment brightblue

The use of period to separate a syntax name from a name specification
is purely a convention used during the development of this feature -
any other format could be used, or the use of an indirect coloring
could be abandoned if it was seen to be excessive. The indirect
coloring mechanism makes sense where multiple syntaxes exist which
intend to use a common color scheme.

The most recent `defcolor` specification wins, which means that any
color definitions in the system nanorc files can be overridden by the
user easily.

Implementation:

The whole feature is behind the ENABLE_COLORNAMES define, configurable
through the `configure` script. It is enabled by default here. Because
the use of the color names would require nanorc files which are not
backward compatible, I chose to protect the feature by an option. The
feature is also more expensive in terms of memory usage whilst reading
the files, so in some environments users may wish to disable the
feature.

The `defcolor` command accumulates the color names that are defined in
a linked list. These color names are checked at definition time to
ensure that they only refer to names that exist - this should avoid
generating a color name that refers to itself, and make it easier to
spot typos by reporting errors early.

The `color` and `icolor` command have been changed to deal with the
new color names in a different way when the options are enabled.
Instead of generating colors (fg, bg, attributes at parse time), the
color name is recorded for later resolution. To record all the
information necessary, a new structure is added to the colortype
which holds the necessary information needed to report the file and
line should the resolution fail for any reason in the later
resolution stage.

The necessity for two stages is because otherwise colors chosed by
the user would be unable to override those that were previously
selected as the files were parsed. It is necessary to ensure that
everywhere that a given color name is used, the colors are updated.

The name of the function that performs the parsing of color names
and returning the colors is renamed to `resolve_color_names` (as it
isn't being used during the parse phase). The first thing that the
function does is to repeatedly replace the color names that were
supplied with those from the color names list generated by
`defcolor`.

Once all the nanorc files have been parsed, a new
`resolve_syntax_colors` function is used to step through all the
syntaxes that were defined, and all the colors within those
syntaxes and resolve the colors. The function restores the
context used by the error reporting system to ensure that any
issues at this stage are reported with the correct file and line.
Once the colornameref has been processed, it is freed - the memory
usage is only during the nanorc parsing.

Multiple colortype uses may refer to the same colornameref (for those
cases where multiple regexs are supplied on a `color` command), so
the structure also includes a reference count to ensure that they
are only freed when the last usage of the colornameref has been
handled.

Within this change, no nanorc files are updated to use the color
naming.

Signed-off-by: Charles Ferguson <gerph@gerph.org>
---
 configure.ac  |  11 ++++
 doc/nano.texi |   8 +++
 doc/nanorc.5  |   6 ++
 src/nano.h    |  29 ++++++++++
 src/rcfile.c  | 155 ++++++++++++++++++++++++++++++++++++++++++++++++--
 5 files changed, 205 insertions(+), 4 deletions(-)

diff --git a/configure.ac b/configure.ac
index 5e73feaf..2544dfeb 100644
--- a/configure.ac
+++ b/configure.ac
@@ -124,6 +124,17 @@ if test "x$enable_color" != xno; then
 	color_support=yes
 fi
 
+AC_ARG_ENABLE(colornames,
+AS_HELP_STRING([--disable-colornames], [Disable naming of colors in syntax highlights]))
+if test "x$enable_colornames" = xyes; then
+	if test "x$enable_color" != xyes; then
+		AC_MSG_ERROR([--enable-colornames needs --enable-color to work])
+	fi
+fi
+if test "x$enable_colornames" != xno; then
+	AC_DEFINE(ENABLE_COLORNAMES, 1, [Define this to allow color names to be defined.])
+fi
+
 AC_ARG_ENABLE(comment,
 AS_HELP_STRING([--disable-comment], [Disable the comment/uncomment function]))
 if test "x$enable_tiny" = xyes; then
diff --git a/doc/nano.texi b/doc/nano.texi
index 5b73c423..f186ab05 100644
--- a/doc/nano.texi
+++ b/doc/nano.texi
@@ -1066,6 +1066,14 @@ This allows syntax highlighting to span multiple lines.
 @item icolor @var{fgcolor},@var{bgcolor} start="@var{fromrx}" end="@var{torx}"
 Same as above, except that the matching is case insensitive.
 
+@item defcolor @var{colorname} @var{colorspec}
+Define a name for a "fgcolor,bgcolor" color specification. The color
+specifications in the @code{color} or @code{icolor} commands may be
+supplied with a color name, which will be expanded to the colour
+specification given in the @code{defcolor} command. This allows the
+syntax coloring to configured with meaningful color names, which users
+can override.
+
 @item include "@var{syntaxfile}"
 Read in self-contained color syntaxes from "syntaxfile".  Note that
 "syntaxfile" may contain only the above commands, from @code{syntax}
diff --git a/doc/nanorc.5 b/doc/nanorc.5
index d4ee2390..48c96696 100644
--- a/doc/nanorc.5
+++ b/doc/nanorc.5
@@ -405,6 +405,12 @@ multiple lines.
 .TP
 .BI "icolor " fgcolor , bgcolor " start=""" fromrx """ end=""" torx """
 Same as above, except that the matching is case insensitive.
+.BI "defcolor " colorname " " colorspec
+Define a name for a color specification. The color specifications in
+the \fBcolor\fP or \fBicolor\fP commands may be supplied with a color
+name, which will be expanded to the colour specification given in the
+\fBdefcolor\fP command. This allows the syntax coloring to configured
+with meaningful color names, which users can override.
 .TP
 .BI "include """ syntaxfile """
 Read in self-contained color syntaxes from \fIsyntaxfile\fP.  Note that
diff --git a/src/nano.h b/src/nano.h
index 31d5cd53..24c3688e 100644
--- a/src/nano.h
+++ b/src/nano.h
@@ -182,6 +182,31 @@ typedef enum {
 } undo_type;
 
 /* Structure types. */
+#ifdef ENABLE_COLORNAMES
+typedef struct colornametype {
+	struct colornametype *next;
+		/* Next item in the colour list */
+	char *name;
+		/* Name that the user gave */
+	char *color;
+		/* The color name (which might be a colorpair) that is used for that name */
+} colornametype;
+
+/* Assigned within the colortype so that any errors during resolving colors can be
+ * reported from the correct location within the source nanorc file.
+ */
+typedef struct colornamereftype {
+	char *name;
+		/* The name they referenced */
+	char *nanorc;
+		/* The file they referenced it from */
+	size_t lineno;
+		/* The line that they referenced it from */
+	size_t count;
+		/* Number of times this block is used */
+} colornamereftype;
+#endif
+
 #ifdef ENABLE_COLOR
 typedef struct colortype {
 	short fg;
@@ -207,6 +232,10 @@ typedef struct colortype {
 		/* Next set of colors. */
 	int id;
 		/* Basic id for assigning to lines later. */
+#ifdef ENABLE_COLORNAMES
+	colornamereftype *colornameref;
+		/* The name associated with this colour, if it has not yet been resolved. */
+#endif
 } colortype;
 
 typedef struct regexlisttype {
diff --git a/src/rcfile.c b/src/rcfile.c
index 4302b1ff..bbeddac6 100644
--- a/src/rcfile.c
+++ b/src/rcfile.c
@@ -144,6 +144,9 @@ static syntaxtype *live_syntax;
 static colortype *lastcolor = NULL;
 		/* The end of the color list for the current syntax. */
 #endif
+#ifdef ENABLE_COLORNAMES
+static colornametype *namedcolors = NULL;
+#endif
 
 /* Report an error in an rcfile, printing it to stderr. */
 void rcfile_error(const char *msg, ...)
@@ -610,13 +613,33 @@ short color_to_short(const char *colorname, bool *bright)
 
 /* Parse the color name (or pair of color names) in the given string.
  * Return FALSE when any color name is invalid; otherwise return TRUE. */
-bool parse_color_names(char *combostr, short *fg, short *bg, int *attributes)
+bool resolve_color_names(char *combostr, short *fg, short *bg, int *attributes)
 {
-	char *comma = strchr(combostr, ',');
+	char *comma;
 	bool bright;
 
+#ifdef ENABLE_COLORNAMES
+	/* Search the list of defined colors for the name they gave, and if found,
+	 * we will use that instead.
+	 * Because the names may supply colors that are themselves color names, we
+	 * keep going until we no longer have any matches. We cannot loop infinitely
+	 * because we already checked that we never forward reference a name during
+	 * definitions.
+	 */
+	colornametype *colorname;
+	do {
+		for (colorname = namedcolors; colorname; colorname = colorname->next) {
+			if (strcasecmp(combostr, colorname->name) == 0)
+				break;
+		}
+		if (colorname)
+			combostr = colorname->color;
+	} while (colorname);
+#endif
+
 	*attributes = A_NORMAL;
 
+	comma = strchr(combostr, ',');
 	if (comma != NULL) {
 		*bg = color_to_short(comma + 1, &bright);
 		if (bright) {
@@ -650,6 +673,9 @@ void parse_colors(char *ptr, int rex_flags)
 	short fg, bg;
 	int attributes;
 	char *item;
+#ifdef ENABLE_COLORNAMES
+	colornamereftype *colornameref;
+#endif
 
 	if (!opensyntax) {
 		rcfile_error(N_("A '%s' command requires a preceding 'syntax' command"),
@@ -664,8 +690,27 @@ void parse_colors(char *ptr, int rex_flags)
 
 	item = ptr;
 	ptr = parse_next_word(ptr);
-	if (!parse_color_names(item, &fg, &bg, &attributes))
+#ifdef ENABLE_COLORNAMES
+	/* When color names are enabled, the resolution of the color names is deferred
+	 * until after all the configuration files are loaded. This allows the named
+	 * colors that are assigned to be updated in the user's configuration to reflect
+	 * their preference.
+	 */
+	bg = fg = 0;
+	attributes = 0;
+
+	colornameref = nmalloc(sizeof(*colornameref));
+	colornameref->name = mallocstrcpy(NULL, item);
+	colornameref->nanorc = mallocstrcpy(NULL, nanorc);
+	colornameref->lineno = lineno;
+	colornameref->count = 0;
+#else
+	/* When color names are disabled, we resolve the colors immediately; this keeps
+	 * the memory usage lower, and means less messing around after the files load.
+	 */
+	if (!resolve_color_names(item, &fg, &bg, &attributes))
 		return;
+#endif
 
 	if (*ptr == '\0') {
 		rcfile_error(N_("Missing regex string after '%s' command"), "color");
@@ -712,6 +757,10 @@ void parse_colors(char *ptr, int rex_flags)
 			newcolor->fg = fg;
 			newcolor->bg = bg;
 			newcolor->attributes = attributes;
+#ifdef ENABLE_COLORNAMES
+			newcolor->colornameref = colornameref;
+			colornameref->count++;
+#endif
 			newcolor->rex_flags = rex_flags;
 
 			newcolor->start_regex = mallocstrcpy(NULL, item);
@@ -769,12 +818,103 @@ void parse_colors(char *ptr, int rex_flags)
 	}
 }
 
+#ifdef ENABLE_COLORNAMES
+/* Parse the definition of a color name in the line at ptr, and add it
+ * to the list of named colors that we can use. */
+void parse_define_color(char *ptr)
+{
+	char *name;
+	char *color;
+	short fg, bg;
+	int attributes;
+	colornametype *colorname;
+
+	if (*ptr == '\0') {
+		rcfile_error(N_("Missing color definition name"));
+		return;
+	}
+
+	name = ptr;
+	ptr = parse_next_word(ptr);
+
+	if (*ptr == '\0') {
+		rcfile_error(N_("Missing color name after '%s' command"), "defcolor");
+		return;
+	}
+
+	color = ptr;
+	ptr = parse_next_word(ptr);
+
+	if (*ptr != '\0') {
+		rcfile_error(N_("Garbage after '%s' command for color '%s'"), "defcolor", name);
+		return;
+	}
+
+	/* We don't care what the color is; just that it parsed; and this also
+	 * prevents the user from creating cycles in the color name definitions,
+	 * because a definition used in the color must already exist before it
+	 * can be used here. Together with the check that we do not duplicate
+	 * the color definition names, this ensures that we cannot create a loop.
+	 */
+	if (!resolve_color_names(color, &fg, &bg, &attributes))
+		return;
+
+	/* Check if the type exists - if it does, we'll override it */
+	for (colorname = namedcolors; colorname; colorname = colorname->next)
+		if (strcasecmp(name, colorname->name) == 0)
+			break;
+
+	/* Re-write existing, or create a new color name entry */
+	if (!colorname) {
+		/* New colorname entry */
+		colorname = nmalloc(sizeof(*colorname));
+		colorname->name = mallocstrcpy(NULL, name);
+		colorname->color = NULL;
+		colorname->next = namedcolors;
+		namedcolors = colorname;
+	}
+	colorname->color = mallocstrcpy(colorname->color, color);
+}
+
+/* Resolve all the colors named in the syntax files to actual colors,
+ * to take account of the user's preference for their colors.
+ */
+void resolve_syntax_colors(void)
+{
+	syntaxtype *syntax;
+	for (syntax = live_syntax; syntax; syntax = syntax->next) {
+		colortype *color;
+		for (color = syntax->color ; color ; color = color->next) {
+			colornamereftype *colornameref = color->colornameref;
+			if (colornameref) {
+				/* Restore the nanorc locations so that errors are reported from the original locations */
+				char *oldnanorc = nanorc;
+				ssize_t oldlineno = lineno;
+				nanorc = colornameref->nanorc;
+				lineno = colornameref->lineno;
+				resolve_color_names(colornameref->name, &color->fg, &color->bg, &color->attributes);
+				nanorc = oldnanorc;
+				lineno = oldlineno;
+				colornameref->count--;
+				if (colornameref->count == 0)
+				{
+					free(colornameref->nanorc);
+					free(colornameref->name);
+					free(colornameref);
+				}
+				color->colornameref = NULL;
+			}
+		}
+	}
+}
+#endif
+
 /* Parse the argument of an interface color option. */
 colortype *parse_interface_color(char *combostr)
 {
 	colortype *trio = nmalloc(sizeof(colortype));
 
-	if (parse_color_names(combostr, &trio->fg, &trio->bg, &trio->attributes)) {
+	if (resolve_color_names(combostr, &trio->fg, &trio->bg, &trio->attributes)) {
 		free(combostr);
 		return trio;
 	} else {
@@ -996,6 +1136,10 @@ void parse_rcfile(FILE *rcstream, bool syntax_only)
 			parse_colors(ptr, NANO_REG_EXTENDED);
 		else if (strcasecmp(keyword, "icolor") == 0)
 			parse_colors(ptr, NANO_REG_EXTENDED | REG_ICASE);
+#ifdef ENABLE_COLORNAMES
+        else if (strcasecmp(keyword, "defcolor") == 0)
+            parse_define_color(ptr);
+#endif
 		else if (strcasecmp(keyword, "linter") == 0)
 			pick_up_name("linter", ptr, &live_syntax->linter);
 		else if (syntax_only && (strcasecmp(keyword, "set") == 0 ||
@@ -1255,6 +1399,9 @@ void do_rcfiles(void)
 	else if (homedir == NULL && xdgconfdir == NULL)
 		rcfile_error(N_("I can't find my home directory!  Wah!"));
 
+#ifdef ENABLE_COLORNAMES
+	resolve_syntax_colors();
+#endif
 	check_vitals_mapped();
 
 #ifdef __linux__
-- 
2.21.0

