class EL_XML_TO_PYXIS_CONVERTER

(source code)

Client examples: XML_TO_PYXIS_APP

description

XML to pyxis converter

note
	description: "XML to pyxis converter"

	author: "Finnian Reilly"
	copyright: "Copyright (c) 2001-2022 Finnian Reilly"
	contact: "finnian at eiffel hyphen loop dot com"

	license: "MIT license (See: en.wikipedia.org/wiki/MIT_License)"
	date: "2023-11-09 14:06:35 GMT (Thursday 9th November 2023)"
	revision: "30"

class
	EL_XML_TO_PYXIS_CONVERTER

inherit
	EL_APPLICATION_COMMAND
		redefine
			description
		end

	EL_MODULE_LIO

	EL_VTD_CONSTANTS

create
	make, make_default

feature {EL_COMMAND_CLIENT} -- Initiliazation

	make (a_source_path: like source_path)
			--
		do
			make_default
			set_source_path (a_source_path)
		end

	make_default
		local
			factory: TP_ZSTRING_FACTORY
		do
			create factory
			create text_matcher.make (
				create {EL_TEXT_MATCHER}.make (
					agent factory.one_of (<< factory.xml_identifier, factory.decimal_constant >>)
				),
				create {EL_TEXT_MATCHER}.make (agent factory.decimal_constant)
			)
			create attributes

			create last_attribute_name.make_empty
			next_node_action := agent put_pyxis_doc
			node_actions := node_actions_table

			create source_path
		end

feature -- Access

	Description: STRING = "Convert XML file to Pyxis format"

	output_path: FILE_PATH

	source_path: FILE_PATH

	source_encoding: EL_MARKUP_ENCODING
		do
			create Result.make_from_file (source_path)
		end

feature -- Element change

	set_source_path (a_source_path: like source_path)
			--
		do
			source_path := a_source_path
			if source_path.has_extension ("ecf") then
				output_path := source_path.with_new_extension ("pecf")
			else
				output_path := source_path.twin
				output_path.add_extension ("pyx")
			end
			create xdoc.make_from_file (source_path)
			if attached xdoc.last_exception as exception and is_lio_enabled then
				exception.put_error (lio)
			end
		end

feature -- Basic operations

	execute
			--
		require else
			convertable: is_convertable
		local
			i, l_type: INTEGER; node_text: ZSTRING
		do
			if is_lio_enabled then
				lio.put_path_field ("Converting", source_path)
				lio.put_new_line
			end
			create out_file.make_open_write (output_path)
			out_file.set_encoding_from_other (source_encoding)
			last_node_type := 0; next_node_type := 0; node_depth := 0; attribute_node_depth := 0
			last_attribute_name.wipe_out
			token_count := xdoc.token_index_upper
			next_node_action := agent put_pyxis_doc
			from i := 1 until i > token_count loop
				node_depth := xdoc.token_depth (i).max (0)
				l_type := xdoc.token_type (i); node_text := xdoc.token_string_8 (i)
				next_node_action (i, l_type, node_text)
				i := i + 1
			end
			out_file.close
		end

feature -- Status query

	is_convertable: BOOLEAN
			-- True if input file is convertable to Pyxis format
		do
			Result := not xdoc.parse_failed
		end

	is_last_node_an_attribute_value: BOOLEAN
		do
			Result := last_node_type = Token.attribute_value or last_node_type = Token.declaration_attribute_value
		end

feature {NONE} -- Parser state actions

	assign_value_to_attribute (i, a_type: INTEGER; node_text: ZSTRING)
		require
			valid_type: Attribute_value_types.has (a_type)
		do
			attributes [last_attribute_name] := node_text
			if i = token_count then
				put_attributes; attributes.wipe_out
				next_node_action := agent try_nothing
			else
				next_node_action := agent save_attribute_name
			end
		end

	call_action_for_type (i, a_type: INTEGER; node_text: ZSTRING)
		do
			node_actions.search (a_type)
			if node_actions.found then
				if i < token_count then
					next_node_type := xdoc.token_type (i + 1)
				end
				node_actions.found_item.call ([node_text])
				last_node_type := a_type

			elseif Attribute_name_types.has (a_type) then
				attribute_node_depth := node_depth + 1
				save_attribute_name (i, a_type, node_text)
			end
		end

	put_pyxis_doc (i, a_type: INTEGER; node_text: ZSTRING)
		require
			is_first_node: i = 1
		do
			out_file.put_string_8 ("pyxis-doc:")
			out_file.put_new_line
			next_node_action := agent call_action_for_type
		end

	save_attribute_name (i, a_type: INTEGER; node_text: ZSTRING)
		do
			if Attribute_name_types.has (a_type) then
				last_attribute_name := node_text.twin
				last_attribute_name.replace_character (':', '.')
				next_node_action := agent assign_value_to_attribute
			else
				put_attributes; attributes.wipe_out
				next_node_action := agent call_action_for_type
				call_action_for_type (i, a_type, node_text)
			end
		end

	try_nothing (i, a_type: INTEGER; node_text: ZSTRING)
		do
		end

feature {NONE} -- Node events

	on_character_data (a_data: ZSTRING)
		local
			lines: LIST [ZSTRING]
		do
			if is_last_node_an_attribute_value then
				out_file.put_new_line
			end
			lines := a_data.split_list ('%N')
			if lines.count > 1 then
				trim_lines (lines)
				if not lines.is_empty then
					put_indent (node_depth + 1); put_line (Triple_quote)
					across lines as line loop
						put_indent (node_depth + 2)
						line.item.left_adjust
						put_line (line.item)
					end
					put_indent (node_depth + 1); put_line (Triple_quote)
				end
			else
				put_indent (node_depth + 1)
				put_line (adjusted_value (lines.first, True, False))
			end
		end

	on_comment_text (a_comment: ZSTRING)
		local
			l_lines: LIST [ZSTRING]
		do
			out_file.put_new_line
			l_lines := a_comment.split_list ('%N')
			trim_lines (l_lines)
			across l_lines as line loop
				line.item.left_adjust
				if line.item.is_empty then
					out_file.put_new_line
				else
					put_indent (node_depth + 1)
					out_file.put_string_8 ("# ")
					put_line (line.item)
				end
			end
		end

	on_starting_tag (a_name: ZSTRING)
		local
			python_name: ZSTRING
		do
			if is_last_node_an_attribute_value then
				out_file.put_new_line
			end
			if not (a_name ~ last_starting_tag and last_node_type = Token.character_data)
				or else next_node_type = Token.attribute_name
			then
				put_indent (node_depth)
				python_name := a_name.twin
				python_name.replace_character (':', '.')
				out_file.put_string (python_name)
				out_file.put_character_8 (':')
				out_file.put_new_line
			end
			last_starting_tag := a_name
		end

feature {NONE} -- Implementation

	adjusted_value (a_string: ZSTRING; identifiers_in_quotes, escape_backslash_before_quote: BOOLEAN): ZSTRING
			-- Put quotes around string unless it looks like a number or identifier
		local
			quote: CHARACTER
		do
			if attached text_matcher [identifiers_in_quotes] as matcher
				and then matcher.is_match (a_string) and not (a_string.is_empty or a_string.has ('-'))
			then
				Result := a_string
			else
				if a_string.index_of ('"', 1) > 0 then
					quote := '%''
				else
					quote := '"'
				end
				create Result.make (a_string.count + 2)
				Result.append_character (quote)
				Result.append (a_string)
				Result.append_character (quote)
				if quote = '"' and escape_backslash_before_quote then
					Result.replace_substring_all (Back_slash_quote, Double_back_slash_quote)
				end
			end
		end

	node_actions_table: EL_HASH_TABLE [PROCEDURE [ZSTRING], INTEGER]
		do
			create Result.make (<<
				[Token.starting_tag, agent on_starting_tag],
				[Token.character_data, agent on_character_data],
				[Token.comment, agent on_comment_text]
			>>)
		end

	put_attributes
		local
			name_value_text, line: ZSTRING
		do
			create line.make (attributes.count * 60)
			across attributes.current_keys as name loop
				name_value_text := name.item + " = " + adjusted_value (attributes [name.item], False, True)
				if line.count + name_value_text.count < 80 then
					if not line.is_empty then
						line.append (Colon_separator)
					end
					line.append (name_value_text)
				else
					if not line.is_empty then
						put_indent (attribute_node_depth)
						put_line (line)
					end
					line := name_value_text
				end
			end
			put_indent (attribute_node_depth)
			put_line (line)
		end

	put_indent (a_node_depth: INTEGER)
		local
			tab_indent: ZSTRING
		do
			create tab_indent.make_filled ('%T', a_node_depth)
			out_file.put_string (tab_indent)
		end

	put_line (a_line: ZSTRING)
		do
			out_file.put_string (a_line)
			out_file.put_new_line
		end

	trim_lines (lines: LIST [ZSTRING])
			-- Remove leading and trailing empty lines
		do
			lines.do_all (agent {ZSTRING}.left_adjust); lines.do_all (agent {ZSTRING}.right_adjust)
			from lines.start until lines.after or not lines.item.is_empty loop
				if lines.item.is_empty then
					lines.remove
				else
					lines.forth
				end
			end
			from lines.finish until lines.before or not lines.item.is_empty loop
				if lines.item.is_empty then
					lines.remove
				end
				lines.back
			end
		end

feature {NONE} -- Internal attributes

	attribute_node_depth: INTEGER

	attributes: EL_ZSTRING_HASH_TABLE [ZSTRING]

	last_attribute_name: ZSTRING

	last_node_type: INTEGER

	last_starting_tag: ZSTRING

	next_node_action: PROCEDURE [INTEGER, INTEGER, ZSTRING]

	next_node_type: INTEGER

	node_actions: like node_actions_table

	node_depth: INTEGER

	out_file: EL_PLAIN_TEXT_FILE

	text_matcher: EL_BOOLEAN_INDEXABLE [EL_TEXT_MATCHER]

	token_count: INTEGER

	xdoc: EL_XML_DOC_CONTEXT

feature {NONE} -- Constants

	Attribute_name_types: ARRAY [INTEGER]
		once
			Result := << Token.attribute_name, Token.declaration_attribute_name, Token.attribute_name_space >>
		end

	Attribute_value_types: ARRAY [INTEGER]
		once
			Result := << Token.attribute_value, Token.declaration_attribute_value >>
		end

	Back_slash_quote: ZSTRING
		once
			Result := "\%""
		end

	Colon_separator: ZSTRING
		once
			Result := "; "
		end

	Double_back_slash_quote: ZSTRING
		once
			Result := "\\%""
		end

	Triple_quote: ZSTRING
		once
			create Result.make_filled ('"', 3)
		end
end