440 lines
20 KiB
Ruby

# Copyright (c) 2018(-2023) STMicroelectronics.
# All rights reserved.
#
# This file is part of the TouchGFX 4.21.2 distribution.
#
# This software is licensed under terms that can be found in the LICENSE file in
# the root directory of this software component.
# If no LICENSE file comes with this software, it is provided AS-IS.
#
###############################################################################/
require 'nokogiri'
require 'rubygems/version'
require 'lib/text_database_parser_4_17'
require 'lib/sanitizer'
require 'lib/version'
require 'lib/xml_reader'
require 'lib/xml_writer'
class TextDatabaseUpgrader
def initialize(file_name, upgrade_version)
@file_name = file_name
@upgrade_version = Gem::Version.new(upgrade_version)
file_ext = File.extname(@file_name)
case file_ext
when '.xlsx'
@xml_doc = Nokogiri::XML::Document.new
@intermediate_version = Gem::Version.new('4.17.0') # or earlier
when '.xml'
@xml_doc = XMLReader.new.read(@file_name)
@intermediate_version = Gem::Version.new(@xml_doc.at('TextDatabase')['Version'])
else
fail "ERROR: Unsupported text database file extension: #{file_ext}"
end
end
def run
# For each version where an upgrade of the text database is required,
# a code snippet, like the 4 lines below, must be added, including
# implementation of the actual UpgradeTo_X_Y class.
# Note! In case of versions where the text database doesn't change,
# nothing has to be done. The text database will automatically be
# updated with the new @upgrade_version.
version_4_18 = Gem::Version.new('4.18.0')
if @intermediate_version < version_4_18 && @upgrade_version >= version_4_18
@xml_doc, @intermediate_version = UpgradeTo_4_18.new(@file_name).run
end
# Now @xml_doc contains 4.18.0 xml data
version_4_19 = Gem::Version.new('4.19.0')
if @intermediate_version < version_4_19 && @upgrade_version >= version_4_19
@xml_doc, @intermediate_version = UpgradeTo_4_19.new(@xml_doc).run
end
# Now @xml_doc contains 4.19.0 xml data
if @xml_doc.at('//TextDatabase')
@xml_doc.at('TextDatabase')['Version'] = @upgrade_version.version
xml_file_name = @file_name.gsub(/\.xlsx$/, '.xml')
XMLWriter.new.write(xml_file_name, @xml_doc)
else
fail "ERROR: Unsupported upgrade version: #{@upgrade_version.version}"
end
end
end
class UpgradeTo_4_19
def initialize(xml_doc)
@xml_doc = xml_doc
@languages = []
@typographies = []
@texts = []
end
def run
xml_doc = Nokogiri::XML::Document.new
xml_doc.encoding = 'utf-8'
create_data_structures
# Create <TextDatabase>
textdatabase_node = xml_doc.add_child(Nokogiri::XML::Node.new('TextDatabase', xml_doc))
textdatabase_node['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance'
textdatabase_node['xsi:noNamespaceSchemaLocation'] = 'texts.xsd'
textdatabase_node['Version'] = @intermediate_version.to_s
# Create <Languages> inside <TextDatabase>
languages_node = textdatabase_node.add_child(Nokogiri::XML::Node.new('Languages', xml_doc))
@languages.each do |language|
# Add <Text> with required attributes inside <Texts>
language_node = languages_node.add_child(Nokogiri::XML::Node.new('Language', xml_doc))
language_node['Id'] = language
end
# Create <Texts> inside <TextDatabase>
texts_node = textdatabase_node.add_child(Nokogiri::XML::Node.new('Texts', xml_doc))
# Create <TextGroup> inside <Texts>
text_group_node = texts_node.add_child(Nokogiri::XML::Node.new('TextGroup', xml_doc))
text_group_node['Id'] = "Group1"
@texts.each do |text|
# Add <Text> with required attributes inside <Texts>
text_node = text_group_node.add_child(Nokogiri::XML::Node.new('Text', xml_doc))
text_node['Id'] = text.id
text_node['TypographyId'] = text.typography_id
text_node['Alignment'] = text.alignment
text.translations.each do |translation|
# Add <Translation> with required attribute
translation_node = text_node.add_child(Nokogiri::XML::Node.new('Translation', xml_doc))
translation_node['Language'] = translation.language
# Add optional attribute
translation_node['Alignment'] = translation.alignment if translation.alignment
# Add actual text
translation_node.add_child(Nokogiri::XML::Text.new(translation.text, xml_doc))
end
end
# Add <Typographies> inside <TextDatabase>
typographies_node = textdatabase_node.add_child(Nokogiri::XML::Node.new('Typographies', xml_doc))
@typographies.each do |typography|
# Add <Typography> with required attributes inside <Typographies>
typography_node = typographies_node.add_child(Nokogiri::XML::Node.new('Typography', xml_doc))
typography_node['Id'] = typography.id
typography_node['Font'] = typography.font_file
typography_node['Size'] = typography.font_size
typography_node['Bpp'] = typography.bpp
typography_node['Direction'] = typography.direction
# Add optional attributes
typography_node['FallbackCharacter'] = typography.fallback_character if typography.fallback_character
typography_node['WildcardCharacters'] = typography.wildcard_characters if typography.wildcard_characters
typography_node['WidgetWildcardCharacters'] = typography.widget_wildcard_characters if typography.widget_wildcard_characters
typography_node['WildcardCharacterRanges'] = typography.wildcard_ranges if typography.wildcard_ranges
typography_node['EllipsisCharacter'] = typography.ellipsis_character if typography.ellipsis_character
# Add <LanguageSetting> with required attributes inside <Typography>
typography.language_settings.each do |language_setting|
language_setting_node = typography_node.add_child(Nokogiri::XML::Node.new('LanguageSetting', xml_doc))
language_setting_node['Language'] = language_setting.language
language_setting_node['Font'] = language_setting.font_file
language_setting_node['Size'] = language_setting.font_size
language_setting_node['Bpp'] = language_setting.bpp
language_setting_node['Direction'] = language_setting.direction
# Add optional attributes
language_setting_node['FallbackCharacter'] = language_setting.fallback_character if language_setting.fallback_character
language_setting_node['WildcardCharacters'] = language_setting.wildcard_characters if language_setting.wildcard_characters
language_setting_node['WidgetWildcardCharacters'] = language_setting.widget_wildcard_characters if language_setting.widget_wildcard_characters
language_setting_node['WildcardCharacterRanges'] = language_setting.wildcard_ranges if language_setting.wildcard_ranges
language_setting_node['EllipsisCharacter'] = language_setting.ellipsis_character if language_setting.ellipsis_character
end
end
return xml_doc, Gem::Version.new('4.19.0')
end
private
class Typography
attr_reader :id, :font_file, :font_size, :bpp, :direction, :fallback_character, :wildcard_characters, :widget_wildcard_characters, :wildcard_ranges, :ellipsis_character, :language_settings, :text_ids
attr_writer :id
def initialize(id, font_file, font_size, bpp, direction, fallback_character, wildcard_characters, widget_wildcard_characters, wildcard_ranges, ellipsis_character)
@id = id
@font_file = font_file
@font_size = font_size
@bpp = bpp
@direction = direction
@fallback_character = fallback_character
@wildcard_characters = wildcard_characters
@widget_wildcard_characters = widget_wildcard_characters
@wildcard_ranges = wildcard_ranges
@ellipsis_character = ellipsis_character
@direction = direction
@language_settings = []
@text_ids = []
end
def add_language_setting(setting)
@language_settings.push(setting)
end
def add_text_id(id)
@text_ids.push(id)
end
def ==(other)
self.id == other.id &&
self.font_file == other.font_file &&
self.font_size == other.font_size &&
self.bpp == other.bpp &&
self.direction == other.direction &&
self.fallback_character == other.fallback_character &&
self.wildcard_characters == other.wildcard_characters &&
self.widget_wildcard_characters == other.widget_wildcard_characters &&
self.wildcard_ranges == other.wildcard_ranges &&
self.ellipsis_character == other.ellipsis_character &&
self.language_settings == other.language_settings
end
alias eql? ==
class LanguageSetting
attr_reader :language, :font_file, :font_size, :bpp, :direction, :fallback_character, :wildcard_characters, :widget_wildcard_characters, :wildcard_ranges, :ellipsis_character
def initialize(language, font_file, font_size, bpp, direction, fallback_character, wildcard_characters, widget_wildcard_characters, wildcard_ranges, ellipsis_character)
@language = language
@font_file = font_file
@font_size = font_size
@bpp = bpp
@direction = direction
@fallback_character = fallback_character
@wildcard_characters = wildcard_characters
@widget_wildcard_characters = widget_wildcard_characters
@wildcard_ranges = wildcard_ranges
@ellipsis_character = ellipsis_character
end
def ==(other)
self.language == other.language &&
self.font_file == other.font_file &&
self.font_size == other.font_size &&
self.bpp == other.bpp &&
self.direction == other.direction &&
self.fallback_character == other.fallback_character &&
self.wildcard_characters == other.wildcard_characters &&
self.widget_wildcard_characters == other.widget_wildcard_characters &&
self.wildcard_ranges == other.wildcard_ranges &&
self.ellipsis_character == other.ellipsis_character
end
alias eql? ==
end
end
class Text
attr_reader :id, :typography_id, :alignment, :translations
attr_writer :typography_id
def initialize(id, typography_id, alignment)
@id = id
@typography_id = typography_id
@alignment = alignment
@translations = []
end
def add_translation(translation)
@translations.push(translation)
end
class Translation
attr_reader :language, :alignment, :text
def initialize(language, alignment, text)
@language = language
@alignment = alignment
@text = text
end
end
end
# Objective for create_data_structure:
# Create data structures that fits the new xml layout and make it easier
# to identify and resolve any upgrading conflicts.
# How it is done:
# Iterate text nodes and create data structures for @languages, @typographies and @texts.
# Note! Unused typographies will bee added at the end.
def create_data_structures
@xml_doc.xpath('/TextDatabase/Texts/Text').each do |text_node|
text_id = text_node['Id']
typo_id = text_node['TypographyId']
alignment = text_node['Alignment']
typography_node = @xml_doc.xpath("/TextDatabase/Typographies/Typography[@Id=\"#{typo_id}\"]")
font_file = typography_node.attr('Font'); font_file = font_file.value if font_file
font_size = typography_node.attr('Size'); font_size = font_size.value if font_size
bpp = typography_node.attr('Bpp'); bpp = bpp.value if bpp
direction = text_node['Direction']
fallback_character = typography_node.attr('FallbackCharacter'); fallback_character = fallback_character.value if fallback_character
wildcard_characters = typography_node.attr('WildcardCharacters'); wildcard_characters = wildcard_characters.value if wildcard_characters
widget_wildcard_characters = typography_node.attr('WidgetWildcardCharacters'); widget_wildcard_characters = widget_wildcard_characters.value if widget_wildcard_characters
wildcard_ranges = typography_node.attr('WildcardCharacterRanges'); wildcard_ranges = wildcard_ranges.value if wildcard_ranges
ellipsis_character = typography_node.attr('EllipsisCharacter'); ellipsis_character = ellipsis_character.value if ellipsis_character
typography = Typography.new(typo_id, font_file, font_size, bpp, direction, fallback_character, wildcard_characters, widget_wildcard_characters, wildcard_ranges, ellipsis_character)
typography.add_text_id(text_id)
text = Text.new(text_id, typo_id, alignment)
text_node.xpath('./Translation').each do |translation_node|
language = translation_node['Language']
alignment = translation_node['Alignment']
typo_id = translation_node['TypographyId']
typography_node = @xml_doc.xpath("/TextDatabase/Typographies/Typography[@Id=\"#{typo_id}\"]")
font_file = typography_node.attr('Font'); font_file = font_file.value if font_file
font_size = typography_node.attr('Size'); font_size = font_size.value if font_size
bpp = typography_node.attr('Bpp'); bpp = bpp.value if bpp
direction = translation_node['Direction']
fallback_character = typography_node.attr('FallbackCharacter'); fallback_character = fallback_character.value if fallback_character
wildcard_characters = typography_node.attr('WildcardCharacters'); wildcard_characters = wildcard_characters.value if wildcard_characters
widget_wildcard_characters = typography_node.attr('WidgetWildcardCharacters'); widget_wildcard_characters = widget_wildcard_characters.value if widget_wildcard_characters
wildcard_ranges = typography_node.attr('WildcardCharacterRanges'); wildcard_ranges = wildcard_ranges.value if wildcard_ranges
ellipsis_character = typography_node.attr('EllipsisCharacter'); ellipsis_character = ellipsis_character.value if ellipsis_character
# Only add language setting if translation specific typography or direction is present
# Note! Default values are used if no specific values exists
if typo_id || direction
language_setting = Typography::LanguageSetting.new(
language,
font_file ? font_file : typography.font_file,
font_size ? font_size : typography.font_size,
bpp ? bpp : typography.bpp,
direction ? direction : typography.direction,
fallback_character ? fallback_character : typography.fallback_character,
wildcard_characters ? wildcard_characters : typography.wildcard_characters,
widget_wildcard_characters ? widget_wildcard_characters : typography.widget_wildcard_characters,
wildcard_ranges ? wildcard_ranges : typography.wildcard_ranges,
ellipsis_character ? ellipsis_character : typography.ellipsis_character,
)
typography.add_language_setting(language_setting)
end
text.add_translation(Text::Translation.new(language, alignment, translation_node.text))
# Only add language if not included in @languages
@languages.push(language) if !@languages.include?(language)
end
# Only add typography if not included in @typographies
if @typographies.include?(typography)
index = @typographies.index(typography)
# Move text_ids from duplicate
# Note! text_ids represents all the texts that uses this specific typography
(@typographies[index].text_ids << typography.text_ids).flatten!
else
@typographies.push(typography)
end
@texts.push(text)
end
# Add unused typographies and give them direction "LTR"
@xml_doc.xpath('/TextDatabase/Typographies/Typography').each do |typo_node|
typo_id = typo_node['Id']
if !@typographies.any? { |typography| typography.id == typo_id}
@typographies.push(Typography.new(typo_id, typo_node['Font'], typo_node['Size'], typo_node['Bpp'], 'LTR', typo_node['FallbackCharacter'], typo_node['WildcardCharacters'], typo_node['WidgetWildcardCharacters'], typo_node['WildcardCharacterRanges'], typo_node['EllipsisCharacter']))
end
end
resolve_any_typography_naming_conflict
end
def resolve_any_typography_naming_conflict
used_typography_ids = @typographies.collect{|typography| typography.id}.uniq
# Group typographies according to id and resolve naming conflicts
@typographies.group_by {|typography| typography.id}.each do |id, group|
group.sort! do |a,b|
# Most used first, or LTR before RTL, or first id occurrence
a.text_ids.length != b.text_ids.length ? b.text_ids.length <=> a.text_ids.length : a.direction != b.direction ? a.direction <=> b.direction : a.text_ids <=> b.text_ids
end
# More than one typography with the same id
if group.length > 1
puts "WARNING: Resolving typography naming conflict for #{id}!"
end
# Do not rename first element, it is already reserved in used_typography_ids
group[1..].each do |typography|
# Rename typography
new_id = id
if typography.direction != group[0].direction
new_id = "#{new_id}_#{typography.direction}"
end
if used_typography_ids.include?(new_id)
suffix = 'A'
while used_typography_ids.include?("#{new_id}_#{suffix}")
suffix.next!
end
new_id = "#{new_id}_#{suffix}"
end
typography_index = @typographies.index(typography)
@typographies[typography_index].id = new_id
# Update texts with new typography ids
@typographies[typography_index].text_ids.each do |text_id|
text_index = @texts.index {|text| text.id == text_id}
@texts[text_index].typography_id = @typographies[typography_index].id
end
used_typography_ids.push(new_id)
end
end
end
end
class UpgradeTo_4_18
def initialize(xlsx_file_name)
@typographies, @text_entries = TextDatabaseParser_4_17.new(xlsx_file_name).run
end
def run
xml_doc = Nokogiri::XML::Document.new
xml_doc.encoding = 'utf-8'
# Create <TextDatabase>
textdatabase_node = xml_doc.add_child(Nokogiri::XML::Node.new('TextDatabase', xml_doc))
textdatabase_node['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance'
textdatabase_node['xsi:noNamespaceSchemaLocation'] = 'texts.xsd'
textdatabase_node['Version'] = @intermediate_version.to_s
# Create <Texts> inside <TextDatabase>
texts_node = textdatabase_node.add_child(Nokogiri::XML::Node.new('Texts', xml_doc))
@text_entries.each do |entry|
# Add <Text> with required attributes inside <Texts>
text_node = texts_node.add_child(Nokogiri::XML::Node.new('Text', xml_doc))
text_node['Id'] = entry.text_id
text_node['TypographyId'] = entry.default_typography
text_node['Alignment'] = entry.default_alignment.capitalize
text_node['Direction'] = entry.default_direction
@text_entries.entries.first.languages.each do |lang|
# Add <Translation> with required attribute
translation_node = text_node.add_child(Nokogiri::XML::Node.new('Translation', xml_doc))
translation_node['Language'] = lang
# Add optional attributes
translation_node['TypographyId'] = entry.typographies[lang] if entry.typographies[lang]
translation_node['Alignment'] = entry.alignments[lang].capitalize if entry.alignments[lang]
translation_node['Direction'] = entry.directions[lang] if entry.directions[lang]
# Add actual text
translation_node.add_child(Nokogiri::XML::Text.new(entry.translation_in(lang).text, xml_doc))
end
end
# Add <Typographies> inside <TextDatabase>
typographies_node = textdatabase_node.add_child(Nokogiri::XML::Node.new('Typographies', xml_doc))
@typographies.each do |typo|
# Add <Typography> with required attributes inside <Typographies>
typography_node = typographies_node.add_child(Nokogiri::XML::Node.new('Typography', xml_doc))
typography_node['Id'] = typo.name
typography_node['Font'] = typo.font_file
typography_node['Size'] = typo.font_size
typography_node['Bpp'] = typo.bpp
# Add optional attributes
typography_node['FallbackCharacter'] = typo.fallback_character if typo.fallback_character
typography_node['WildcardCharacters'] = typo.wildcard_characters if typo.wildcard_characters
typography_node['WidgetWildcardCharacters'] = typo.widget_wildcard_characters if typo.widget_wildcard_characters
typography_node['WildcardCharacterRanges'] = typo.wildcard_ranges if typo.wildcard_ranges
typography_node['EllipsisCharacter'] = typo.ellipsis_character if typo.ellipsis_character
end
return xml_doc, Gem::Version.new('4.18.0')
end
end