# JOE Syntax-Highlighting Description
#                 for
# JOE Syntax-Highlighting Descriptions
#
# Author: Charles J. Tabony
# Date:   2007-2-13
#
# This is a highlighting description for files like this one.
#
# When CHECKING is defined, it is very aggressive about error checking.  The
# idea is that anywhere the highlighted file contains a syntax error, at least
# one visible character should be highlighted as Bad.  While that feature is
# useful for finding syntax errors, it is annoying when editing a file, since
# nearly everything is an error until you finish typing it.
#
# In order to not annoy people by default, but keep the option of strictly
# checking syntax, I predicated the stricter checking on the CHECKING parameter. 
# By default, things that are incomplete are generally not marked as errors. 
# Only things that appear to be actual mistakes are highlighted as Bad.  To
# enable the stricter checking, one can highlight the file with the jsf_check
# syntax.  jsf_check.jsf simply calls the entire jsf.jsf file with CHECKING
# defined.
#
# The idea is for authors of a jsf file to edit their file, highlight it with
# jsf_check, and then look for any red characters.  That way they can check for
# syntax errors before testing the changes.




#####################
# Color Definitions #
#####################

=Idle
=Comment	green
=Conditional	blue
=Parameter	bold blue
=Keyword	bold
=Color		yellow
=StandardColor	bold
=State
=Subr		magenta
=Literal	cyan
=Escape		bold cyan
=Bad		bold red




##################
# Initial States #
##################

# This is a dummy state that simply jumps to comment_or_bad.  It is here so that
# when this file calls itself with the STRINGS parameter defined, comment_or_bad
# will effectively be the initial state.  comment_or_bad should be the initial
# state because strings and istrings options can only be used as the last option
# of a transition.
.ifdef STRINGS
:strings_initial Idle
	*		comment_or_bad		noeat
.endif

# Each new line (that is not considered bad from the beginning) begins in the
# idle state.  The first non-whitespace character determines what the rest of
# the line should contain.  Following a strings or istrings option, only strings
# and comments are allowed until the word "done" denotes the end of the list.
:idle Idle
	*		bad			noeat
	" \t\n"		idle
.ifdef STRINGS
.else
	"-"		sync_lines_first
	"."		conditional_first	mark recolor=-1
	"="		color_definition_first
	":"		state_first
	"*&"		special_character	recolor=-1
.endif
	"\""		string			recolor=-1
.ifdef STRINGS
	"A-Za-z_"	special_word		mark recolor=-1 buffer
.endif
	"#"		comment			recolor=-1


##############
# Sync Lines #
##############

# Following a '-' should be either the number of sync lines or nothing (meaning
# unlimited).  Nothing else other than a comment should appear on the same line.
.ifdef STRINGS
# A sync lines directive should not appear between "[i]strings" and "done".
.else
# If we see a non-digit or a '0', then we have seen the entire sync lines
# directive.  The only thing that may appear on the rest of the line is a
# comment.  Otherwise there may be more digits in the number.
:sync_lines_first Literal
	*		comment_or_bad		noeat
	"0"		comment_or_bad
	"1-9"		sync_lines

# Highlight the remainder of the number.
:sync_lines Literal
	*		comment_or_bad		noeat
	"0-9"		sync_lines
.endif


##########################
# Conditional Directives #
##########################

# Following a '.' should be a conditional directive.
.ifdef STRINGS
# A conditional directive should not appear between "[i]strings" and "done".
.else
# Start buffering the conditional directive.
:conditional_first Conditional
	*		conditional		noeat buffer

# Recognize the set of conditional directives.
:conditional Idle
	*		conditional_unknown	noeat strings
	"ifdef"		ifdef_color
	"else"		conditional_color
	"endif"		conditional_color
	"subr"		subr_color
	"end"		conditional_color
	done
	"A-Za-z0-9_"	conditional

# We encountered what looks like a conditional directive but is unrecognized as
# such.
:conditional_unknown Idle
.ifdef CHECKING
	*		bad_line		recolormark noeat
.else
	*		comment_or_bad		noeat
.endif

# We saw a conditional directive that does not take an argument.  Nothing else
# other than a comment should appear on the same line.
:conditional_color Conditional
	*		comment_or_bad		noeat

# We saw a ".ifdef" which must be followed by a parameter.
:ifdef_color Conditional
	*		need_parameter		noeat

# We loop over whitespace until we see the first character of the parameter.
:need_parameter Idle
	*		bad			noeat
	" \t"		need_parameter
	"A-Za-z_"	parameter		recolor=-1

# Now we highlight the remainder of the parameter.
:parameter Parameter
	*		comment_or_bad		noeat
	"A-Za-z0-9_"	parameter

# The following three states are identical to the previous three except the
# color.
:subr_color Conditional
	*		need_subr		noeat

:need_subr Idle
	*		bad			noeat
	" \t"		need_subr
	"A-Za-z_"	subr			recolor=-1

:subr Subr
	*		comment_or_bad		noeat
	"A-Za-z0-9_"	subr
.endif


####################
# Color Definition #
####################

# Following an '=' should be a color definition.
.ifdef STRINGS
# Color definitions should not appear between "[i]strings" and "done".
.else
# A color name must have at least one character.
:color_definition_first Color
	*		color_definition
	" \t#\n"	bad			noeat

# Highlight any remaining characters until we see whitespace, a comment, or a
# newline.
:color_definition Color
	*		color_definition
	" \t#\n"	colors_ws		noeat

# The color name may be followed by zero or more standard colors or attributes,
# ending in a comment or newline.
:colors_ws Idle
	*		color_bad		recolor=-1
	" \t"		colors_ws
	"A-Za-z_"	color			mark recolor=-1 buffer
	"#\n"		comment			noeat

# Here we recognize the attributes and standard color names.  None of the
# attributes or standard color names contain a digit except fg_NNN and bg_NNN,
# which are handled specially below.
:color Idle
	*		color_unknown		noeat strings
	"inverse"	color_color
	"underline"	color_color
	"bold"		color_color
	"italic"	color_color
	"blink"		color_color
	"dim"		color_color
	"white"		color_color
	"cyan"		color_color
	"magenta"	color_color
	"blue"		color_color
	"yellow"	color_color
	"green"		color_color
	"red"		color_color
	"black"		color_color
	"bg_white"	color_color
	"bg_cyan"	color_color
	"bg_magenta"	color_color
	"bg_blue"	color_color
	"bg_yellow"	color_color
	"bg_green"	color_color
	"bg_red"	color_color
	"bg_black"	color_color
	"WHITE"		color_color
	"CYAN"		color_color
	"MAGENTA"	color_color
	"BLUE"		color_color
	"YELLOW"	color_color
	"GREEN"		color_color
	"RED"		color_color
	"BLACK"		color_color
	"bg_WHITE"	color_color
	"bg_CYAN"	color_color
	"bg_MAGENTA"	color_color
	"bg_BLUE"	color_color
	"bg_YELLOW"	color_color
	"bg_GREEN"	color_color
	"bg_RED"	color_color
	"bg_BLACK"	color_color
	"fg_"		color_number_first
	"bg_"		color_number_first
	done
	"A-Za-z_"	color

# We encountered what looks like a standard color but is unrecognized as such.
:color_unknown Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		colors_ws		noeat
.endif

# Here we have seen either "fg_" or "bg_".  We now expect to find a number. The
# number should either be a one to two digit number, representing greyscale
# intensity, in the range 0-23, or a three digit number, where each digit is in
# the range 0-5 and represents the intensity of red, green, and blue
# respectively.
:color_number_first Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		color_bad		noeat
.endif
	"0"		color_zero
	"1"		color_one
	"2"		color_two
	"3-5"		color_number_second
	"6-9"		color_end

# The first digit is a zero, thus we either have a greyscale intensity of 0, in
# which case we should not see any more digits, or we have the first RGB digit,
# in which case we should see two more ditits in the range 0-5.
:color_zero Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		color_bad		noeat
.endif
	"0-5"		color_rgb_third
	" \t#\n"	color_color		recolormark noeat

# The first digit is a one.  If we see whitespace or a comment, then we have a
# greyscale intensity of 1.  If we see a digit 6-9, then we have a greyscale
# intensity of 16-19.  If we see a digit 0-5, then we either have a greyscale
# intensity of 10-15 or an RGB value.
:color_one Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		color_bad		noeat
.endif
	"0-5"		color_number_third
	"6-9"		color_end
	" \t#\n"	color_color		recolormark noeat

# The first digit is a two.  If we see whitespace or a comment, then we have a
# greyscale intensity of 2.  If we see a digit 4-5, then we have the first two
# digits of an RGB value.  If we see a digit 0-3, then we either have a
# greyscale intensity of 20-23 or an RGB value.
:color_two Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		color_bad		noeat
.endif
	"0-3"		color_number_third
	"4-5"		color_rgb_third
	" \t#\n"	color_color		recolormark noeat

# We have seen one digit that could be either the greyscale intensity or the
# first RGB digit.  If we see any more digits, they we must have an RGB value,
# because otherwise the number would be outside the range 0-23.
:color_number_second Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		color_bad		noeat
.endif
	"0-5"		color_rgb_third
	" \t#\n"	color_color		recolormark noeat

# We have seen two digits that could be either the greyscale intensity or the
# first two RGB digits.  If we see any more digits, they we must have an RGB
# value, because otherwise the number would be outside the range 0-23.
:color_number_third Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		color_bad		noeat
.endif
	"0-5"		color_end
	" \t#\n"	color_color		recolormark noeat

# We have seen two digits, both 0-5, that either start with zero or are outside
# the range 0-23.  Thus we expect a third 0-5 digit.
:color_rgb_third Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		color_bad		noeat
.endif
	"0-5"		color_end

# We have seen "fg_" or "bg_" followed by one to three digits.  Any more digits
# would either be too many or make the number out of range.  We now expect to
# see whitespace, a comment, or a newline.
:color_end Idle
.ifdef CHECKING
	*		color_bad		recolormark noeat
.else
	*		color_bad		noeat
.endif
	" \t#\n"	color_color		recolormark noeat

# This is a dummy state that simply provides the highlighting color for the
# standard color or attribute and jumps to colors_ws without consuming any
# characters.
:color_color StandardColor
	*		colors_ws		noeat

# We have encountered something that is not recognized as a standard color or
# attribute.  Continue to highlight characters as Bad until we see whitespace, a
# comment, or a newline.
:color_bad Bad
	*		color_bad
	" \t#\n"	colors_ws		noeat
.endif


#########
# State #
#########

# Following a ':' should be a state definition.
.ifdef STRINGS
# New states should not appear between "[i]strings" and "done".
.else
# A state name must begin with an alpha character or an underscore.
:state_first State
	*		bad			noeat
	"A-Za-z_"	state

# Subsequent characters in a state name must be alpha-numeric or underscores.
:state State
	*		bad			noeat
	"A-Za-z0-9_"	state
	" \t"		need_state_color	recolor=-1

# A state must have a color.
:need_state_color Idle
	*		state_color		recolor=-1
	" \t"		need_state_color
	"#\n"		bad			noeat

# Highlight any remaining characters until we see whitespace, a comment, or a
# newline.
:state_color Color
	*		state_color
	" \t"		context_ws		recolor=-1
	"#\n"		comment_or_bad		noeat

# Following the state color, there might be one or more contexts.  Loop over
# whitespace until we find something else.
:context_ws Idle
	*		comment_or_bad		noeat
	" \t"		context_ws
	"A-Za-z_"	context			mark recolor=-1 buffer

# Here we recognize the possible contexts.
:context Idle
	*		context_unknown		noeat strings
	"comment"	context_color
	"string"	context_color
	done
	"A-Za-z0-9_"	context

# We encountered what looks like a context but is unrecognized as such.
:context_unknown Idle
.ifdef CHECKING
	*		context_bad		recolormark noeat
.else
	*		context_ws		noeat
.endif

# We encountered a valid context.
:context_color Keyword
	*		context_ws		noeat

# We saw something that is not a valid context name with checking enabled.
# Continue to highlight it as Bad until we see whitespace or a comment.
:context_bad Bad
	*		context_bad
	" \t#\n"	context_ws		noeat
.endif


##############
# Transition #
##############

# A state transition starts with a '*', an '&', or a string.
.ifdef STRINGS
# Transitions must start with a string between "[i]strings" and "done".
.else
# We saw either a '*' or an '&'.  Now we need the next state.
:special_character Keyword
	*		need_next_state		noeat
.endif

# We are in a string.  Continue until we see the close quote or a newline.
# Highlight escaped characters within the string differently.  They start with a
# '\'.
:string Literal string
	*		string
	"\\"		escape			recolor=-1
	"\""		need_next_state
.ifdef CHECKING
	"\n"		bad
.else
	"\n"		bad			noeat
.endif

# Highlight an escaped character within a string.
:escape Escape string
	*		string

# Loop over whitespace until we see the first character of the next state.
:need_next_state Idle
	*		bad			noeat
	" \t"		need_next_state
	"A-Za-z_"	next_state		recolor=-1

# Now we highlight the remainder of the next state.
:next_state State
	*		bad			noeat
	"A-Za-z0-9_"	next_state
	" \t"		options_ws
	"#\n"		comment			noeat

# Following the next state should be zero or more options.  Loop over whitespace
# until we find an option, comment, or newline.
:options_ws Idle
	*		option_bad		recolor=-1
	" \t"		options_ws
	"A-Za-z_"	option			mark recolor=-1 buffer
	"#\n"		comment			noeat

# Here we recognize the possible options.  The strings and istrings options
# cannot be used between "[i]strings" and "done".  Since conditional directives
# cannot be used between "[i]strings" and "done" either, the list must be
# duplicated, once without and once with the strings and istrings options.
:option Idle
.ifdef STRINGS
	*		option_unknown		recolormark noeat strings
	"noeat"		option_color
	"recolor"	recolor_color
	"mark"		option_color
	"markend"	option_color
	"recolormark"	option_color
	"buffer"	option_color
	"save_c"	option_color
	"save_s"	option_color
	"hold"		option_color
	"call"		call_color
	"return"	option_color
	"reset"		option_color
	done
.else
	*		option_unknown		recolormark noeat strings
	"noeat"		option_color
	"recolor"	recolor_color
	"mark"		option_color
	"markend"	option_color
	"recolormark"	option_color
	"buffer"	option_color
	"save_c"	option_color
	"save_s"	option_color
	"strings"	strings_color
	"istrings"	strings_color
	"hold"		option_color
	"call"		call_color
	"return"	option_color
	"reset"		option_color
	done
.endif
	"A-Za-z0-9_"	option

# We encountered what looks like an option but is unrecognized as such.
:option_unknown Idle
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif

# We have encountered an option that does not take an argument.  Highlight it
# and continue to look for more options.
:option_color Keyword
	*		options_ws		noeat

.ifdef STRINGS
# The strings and istrings options cannot be used between "[i]strings" and
# "done".
.else
# The strings and istrings options are followed by a list of transitions. 
# Rather than duplicate all of the states that highlight transitions, we call
# this entire file as a subroutine and use the STRINGS parameter to disable
# everything else and enable the done keyword.  We return to the comment_or_bad
# state since we will return after seeing the done keyword, and nothing but a
# comment should follow the done keyword.
:strings_color Keyword
	*		comment_or_bad		noeat call=jsf(STRINGS)
.endif

# Highlight the recolor option.
:recolor_color Keyword
	*		recolor_equal		noeat

# The recolor option must be followed by an '='.  Loop over whitespace until we
# find one.
:recolor_equal Idle
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif
	" \t"		recolor_equal
	"="		recolor_minus		mark

# The recolor option takes an integer argument, and that integer must be
# negative.  Thus the '=' must be followed by a minus sign.  Loop over
# whitespace until we find one.
:recolor_minus Idle
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif
	" \t"		recolor_minus
	"-"		recolor_amount_first	mark recolor=-1

# The first digit of the argument to recolor must be non-zero.
:recolor_amount_first Literal
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		recolormark noeat
	"0"		option_bad		recolormark noeat
.endif
	"1-9"		recolor_amount

# Keep highlighting digits until we see something else.
:recolor_amount Literal
	*		option_bad		recolormark recolor=-1
	"0-9"		recolor_amount
	" \t#\n"	options_ws		noeat

# Highlight the call option.
:call_color Keyword
	*		call_equal		noeat

# The call option must be followed by an '='.  Loop over whitespace until we
# find one.
:call_equal Idle
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif
	" \t"		call_equal
	"="		call_file_or_dot	mark

# The first part of the argument to the call option is the name of the file
# containing the subroutine or a '.', implying the current file.  Loop over
# whitespace until we see one of those two things.
:call_file_or_dot Idle
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif
	" \t"		call_file_or_dot
	"A-Za-z_"	call_file		mark recolor=-1
	"."		call_dot		mark

# Highlight the remainder of the file name.  The file name can be followed by a
# '.', which must then be followed by the name of a subroutine, or by a list of
# parameters in parentheses.  The '.', if present, cannot have whitespace on
# either side.
:call_file Subr
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif
	"A-Za-z0-9_"	call_file
	"."		call_dot		mark recolor=-1
	" \t("		call_open_paren		noeat

# We saw a '.'.  The next character must start the name of a subroutine.
:call_dot Idle
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif
	"("		call_dot_bad		recolormark noeat
	"A-Za-z_"	call_subr		mark recolor=-1

# We have seen a dot followed by an open parenthesis.  A dot must be followed by
# a subroutine name.  Highlight the dot as Bad.
:call_dot_bad Bad
	*		call_open_paren		noeat

# Highlight the remainder of the subroutine name.  Following the subroutine name
# must be a list of parameters in parentheses, possibly preceded by whitespace.
:call_subr Subr
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif
	"A-Za-z0-9_"	call_subr
	" \t("		call_open_paren		noeat

# Loop over whitespace until we find the open parenthesis.
:call_open_paren Idle
.ifdef CHECKING
	*		option_bad		recolormark noeat
.else
	*		options_ws		noeat
.endif
	" \t"		call_open_paren
	"("		call_parameters_ws

# The list of parameters is delimited by whitespace.  Loop over whitespace until
# we find either the beginning of a parameter or a close parenthesis.  We should
# not see a comment or newline since the list should be terminated by a close
# parenthesis.
:call_parameters_ws Idle
	*		call_parameter_bad	recolor=-1
	" \t"		call_parameters_ws
	"-"		call_parameter_undef
	"A-Za-z_"	call_parameter		recolor=-1
	")"		options_ws
	"#\n"		bad			noeat

# We saw a "-".  The next character should start the parameter being undefined.
:call_parameter_undef Parameter
	*		call_parameters_ws	noeat
	"A-Za-z_"	call_parameter		recolor=-2

# Highlight the remainder of the parameter.
:call_parameter Parameter
	*		call_parameters_ws	noeat
	"A-Za-z0-9_"	call_parameter

# We saw something that is not a valid parameter name.  Continue to highlight it
# as Bad until we see whitespace.
:call_parameter_bad Bad
	*		call_parameter_bad
	") \t#\n"	call_parameters_ws	noeat

# We saw something that is not a valid option name.  Continue to highlight it as
# Bad until we see whitespace or a comment.
:option_bad Bad
	*		option_bad
	" \t#\n"	options_ws		noeat


########
# Done #
########

.ifdef STRINGS
# The special word, "done", can only be used after a strings or istrings option.
# Recognize the done keyword.
:special_word Idle
	*		bad_line		recolormark noeat strings
	"done"		done_color
	done
	"A-Za-z0-9_"	special_word

# Highlight the done keyword and return to highlighting things normally, since
# the list of strings has been terminated.
:done_color Keyword
	*		comment_or_bad		return noeat
.endif


##################
# Comment or Bad #
##################

# We have seen everything that should appear on the current line except an
# optional comment.  Loop over whitespace until we find a comment or newline.
:comment_or_bad Idle
	*		bad			noeat
	" \t"		comment_or_bad
	"#\n"		comment			noeat


###########
# Comment #
###########

# Continue to highlight the comment until the end of the line.
:comment Comment comment
	*		comment
	"\n"		idle


#######
# Bad #
#######

.ifdef CHECKING
# We have encountered incorrect syntax.  Loop over whitespace until we see the
# first visible character.  Highlight that character and the rest of the line as
# Bad.
:bad Bad
	*		bad_line
	" \t\n"		bad
.else
# When not performing strict checking, don't go searching for the next visible
# character to highlight as Bad.  Simply highlight the rest of the line as Bad,
# even if it is invisible.
:bad Bad
	*		bad_line		noeat
.endif

# Continue to highlight everything as Bad until the end of the line.
:bad_line Bad
	*		bad_line
	"\n"		idle
