<commit_before># A `Presentation` represents a `.uip` presentation, created and edited by UI Composer Studio.
class UIC::Presentation
	include UIC::XMLFileBacked

	# Create a new presentation. If you do not specify the `uip_path` to load from, you must
	# later set the `.file = `for the presentation, and then call the {#load_from_file} method.
	# @param uip_path [String] path to the `.uip` to load.
	def initialize( uip_path=nil )
		@doc = Nokogiri.XML('<UIP version="3"><Project><Graph><Scene id="Scene"/></Graph><Logic><State name="Master Slide" component="#Scene"/></Logic></Project></UIP>')
		@graph = @doc.at('Graph')
		@scene = @graph.at('Scene')
		@logic = @doc.at('Logic')
		@missing_classes = []
		@asset_by_el  = {} # indexed by asset graph element
		@slides_for   = {} # indexed by asset graph element
		@slides_by_el = {} # indexed by slide state element
		self.file = uip_path if uip_path
	end

	# Load information for the presentation from disk.
	# If you supply a path to a `.uip` file when creating the presentation
	# this method is automatically called.
	#
	# @return [nil]
	def on_doc_loaded
		@graph = @doc.at('Graph')
		@scene = @graph.at('Scene')
		@logic = @doc.at('Logic')

		generate_custom_classes
		rebuild_caches_from_document

		nil
	end

	# @return [String] the xml representation of this presentation. Formatted to match UI Composer Studio's formatting as closely as possible (for minimal diffs after update).
	def to_xml
		doc.to_xml( indent:1, indent_text:"\t" )
		   .gsub( %r{(<\w+(?: [\w:]+="[^"]*")*)(/?>)}i, '\1 \2' )
		   .sub('"?>','" ?>')
	end

	# Scan the .uip for custom classes that will be needed to generate assets.
	# Called once during initial load of the file.
	# @return [nil]
	def generate_custom_classes
		# TODO: this method assumes an application to find the metadata on; the metadata should be part of this class instance instead, shared with the app when present
		parent_class_name = {
			'CustomMaterial' => 'MaterialBase',
			'Effect'         => 'Effect',
			'Behavior'       => 'Behavior'
		}
		@class_by_ref = {}
		@doc.xpath('/UIP/Project/Classes/*').each do |reference|
			path = absolute_path( reference['sourcepath'] )
			next unless File.exist?( path )
			parent_class = app.metadata.by_name[ parent_class_name[reference.name] ]
			parent_props = parent_class.properties
			new_defaults = Hash[ reference.attributes.map{ |name,attr| [name,attr.value] }.select{ |name,val| parent_props[name] } ]
			property_el = case reference.name
				when 'CustomMaterial', 'Effect'
					Nokogiri.XML(File.read(path,encoding:'utf-8')).at('/*/MetaData')
				when 'Behavior'
					lua  = File.read(path,encoding:'utf-8')
					meta = lua[ /--\[\[(.+?)(?:--)?\]\]/m, 1 ]
					Nokogiri.XML("<MetaData>#{meta}</MetaData>").root
			end
			@class_by_ref[ "##{reference['id']}" ] = app.metadata.create_class( property_el, parent_class, reference.name, new_defaults )
		end
		nil
	end

	# Update the presentation to be in-sync with the document.
	# Must be called whenever the in-memory representation of the XML document is changed.
	# Called automatically by all necessary methods; only necessary if script (dangerously)
	# manipulates the `.doc` of the presentation directly.
	#
	# @return [nil]
	def rebuild_caches_from_document<commit_msg>Add support for RenderPlugin as a custom class<commit_after># A `Presentation` represents a `.uip` presentation, created and edited by UI Composer Studio.
class UIC::Presentation
	include UIC::XMLFileBacked

	# Create a new presentation. If you do not specify the `uip_path` to load from, you must
	# later set the `.file = `for the presentation, and then call the {#load_from_file} method.
	# @param uip_path [String] path to the `.uip` to load.
	def initialize( uip_path=nil )
		@doc = Nokogiri.XML('<UIP version="3"><Project><Graph><Scene id="Scene"/></Graph><Logic><State name="Master Slide" component="#Scene"/></Logic></Project></UIP>')
		@graph = @doc.at('Graph')
		@scene = @graph.at('Scene')
		@logic = @doc.at('Logic')
		@missing_classes = []
		@asset_by_el  = {} # indexed by asset graph element
		@slides_for   = {} # indexed by asset graph element
		@slides_by_el = {} # indexed by slide state element
		self.file = uip_path if uip_path
	end

	# Load information for the presentation from disk.
	# If you supply a path to a `.uip` file when creating the presentation
	# this method is automatically called.
	#
	# @return [nil]
	def on_doc_loaded
		@graph = @doc.at('Graph')
		@scene = @graph.at('Scene')
		@logic = @doc.at('Logic')

		generate_custom_classes
		rebuild_caches_from_document

		nil
	end

	# @return [String] the xml representation of this presentation. Formatted to match UI Composer Studio's formatting as closely as possible (for minimal diffs after update).
	def to_xml
		doc.to_xml( indent:1, indent_text:"\t" )
		   .gsub( %r{(<\w+(?: [\w:]+="[^"]*")*)(/?>)}i, '\1 \2' )
		   .sub('"?>','" ?>')
	end

	# Scan the .uip for custom classes that will be needed to generate assets.
	# Called once during initial load of the file.
	# @return [nil]
	def generate_custom_classes
		# TODO: this method assumes an application to find the metadata on; the metadata should be part of this class instance instead, shared with the app when present
		parent_class_name = {
			'CustomMaterial' => 'MaterialBase',
			'Effect'         => 'Effect',
			'Behavior'       => 'Behavior',
			'RenderPlugin'   => 'RenderPlugin'
		}
		@class_by_ref = {}
		@doc.xpath('/UIP/Project/Classes/*').each do |reference|
			path = absolute_path( reference['sourcepath'] )
			next unless File.exist?( path )
			parent_class = app.metadata.by_name[ parent_class_name[reference.name] ]
			raise "Error, unsupported custom class #{reference.name}" unless parent_class
			parent_props = parent_class.properties
			new_defaults = Hash[ reference.attributes.map{ |name,attr| [name,attr.value] }.select{ |name,val| parent_props[name] } ]
			property_el = case reference.name
				when 'CustomMaterial', 'Effect'
					Nokogiri.XML(File.read(path,encoding:'utf-8')).at('/*/MetaData')
				when 'Behavior'
					lua  = File.read(path,encoding:'utf-8')
					meta = lua[ /--\[\[(.+?)(?:--)?\]\]/m, 1 ]
					Nokogiri.XML("<MetaData>#{meta}</MetaData>").root
			end
			@class_by_ref[ "##{reference['id']}" ] = app.metadata.create_class( property_el, parent_class, reference.name, new_defaults )
		end
		nil
	end

	# Update the presentation to be in-sync with the document.
	# Must be called whenever the in-memory representation of the XML document is changed.
	# Called automatically by all necessary methods; only necessary if script (dangerously)
	# manipulates the `.doc` of the presentation directly.
	#
	# @return [nil]
	def rebuild_caches_from_document
{'fraction_non_alphanumeric': 0.1382488479262673, 'fraction_numerical': 0.002081165452653486, 'mean_word_length': 4.096969696969697, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 40, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 6}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4505735', 'n_tokens_mistral': 2330, 'n_tokens_neox': 2149, 'n_words': 666}