mrackwitz/xcres

View on GitHub
lib/xcres/builder/resources_builder.rb

Summary

Maintainability
B
6 hrs
Test Coverage
require 'xcres/builder/file_builder'
require 'xcres/helper/file_helper'

class XCRes::ResourcesBuilder < XCRes::FileBuilder

  include XCRes::FileHelper

  BANNER = <<EOS
// generated by xcres
//
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
// See https://github.com/mrackwitz/xcres for more info.
//
EOS

  OBJC_COMPILER_KEYWORDS = %w{
    auto break case char const continue default do double else enum extern float
    for goto if inline int long register restrict return short signed sizeof
    static struct switch typedef union unsigned void volatile while
  }

  SWIFT_COMPILER_KEYWORDS = %w{
    class deinit enum extension func import init internal let operator private
    protocol public static struct subscript typealias var break case continue
    default do else fallthrough for if in return switch where while as
    dynamicType false is nil self Self super true
  }

  SWIFT_EXTENSIONS = <<EOS
public extension %s.Strings {
    public var localizedValue: String {
        return NSLocalizedString(self.rawValue,
                                 bundle: NSBundle(forClass: R.self),
                                 comment: "")
    }
}
EOS

  # @return [String]
  #         the name of the constant in the generated file(s)
  attr_accessor :resources_constant_name

  # @return [Bool]
  #         whether the generated resources constant should contain inline
  #         documentation for each key, true by default
  attr_accessor :documented
  alias :documented? :documented

  # @return [Bool]
  #         whether Swift code should be generated,
  #         Objective-C used if false, false by default
  attr_accessor :swift
  alias :swift? :swift

  # @return [Hash{String => {String => String}}]
  #         the sections, which will been written to the built files
  attr_reader :sections

  # Initialize a new instance
  #
  def initialize
    @sections = {}
    self.documented = true
    self.swift = false
  end

  # Extract resource name from #output_path, if not customized
  #
  # @return [String]
  #
  def resources_constant_name
    @resources_constant_name ||= basename_without_ext output_path
  end

  def add_section name, items, options = {}
    raise ArgumentError.new 'No items given!' if items.nil?

    transformed_items = {}

    for key, value in items
      transformed_key = transform_key key, options

      # Skip invalid key names
      if transformed_key.length == 0
        logger.warn "Skip invalid key: '%s'. (Was transformed to empty text)", key
        next
      end

      # Skip compiler keywords
      compiler_keywords = swift? ? SWIFT_COMPILER_KEYWORDS : OBJC_COMPILER_KEYWORDS
      if compiler_keywords.include? transformed_key
        logger.warn "Skip invalid key: '%s'. (Was transformed to keyword '%s')", key, transformed_key
        next
      end

      transformed_items[transformed_key] = value
    end

    @sections[name] = transformed_items
  end

  def build
    super

    if swift?
      build_and_write_swift
    else
      build_and_write_objc
    end
  end

  protected

    def transform_key key, options
      # Split the key into components
      components = key.underscore.split /[\_\.\-\/ ]/

      # Build the new key incremental
      result = ''

      for component in components
        # Ignore empty components
        next unless component.length > 0

        # Ignore components which are already contained in the key, if enabled
        if options[:shorten_keys]
          next unless result.downcase.scan(component).blank?
        end

        # Clean component from non alphanumeric characters
        clean_component = component.gsub /[^a-zA-Z0-9]/, ''

        # Skip if empty
        next unless clean_component.length > 0

        if result.length == 0
          result += clean_component
        else
          result += clean_component[0].upcase + clean_component[1..-1]
        end
      end

      # If the first character is not a letter, prefix it with an underscore
      result.gsub(/^[^_a-z]/i, '_\0')
    end

    def build_and_write_swift
      # Build file contents and write them to disk
      write_file_eventually "#{output_path}.swift", (build_contents do |swift_file|
        build_swift_contents swift_file
      end)
    end

    def build_and_write_objc
      # Build file contents and write them to disk
      write_file_eventually "#{output_path}.h", (build_contents do |h_file|
        build_header_contents h_file
      end)

      write_file_eventually "#{output_path}.m", (build_contents do |m_file|
        build_impl_contents m_file
      end)
    end

    def build_header_contents h_file
      h_file.writeln BANNER
      h_file.writeln
      h_file.writeln '#import <Foundation/Foundation.h>'
      h_file.writeln
      h_file.writeln 'FOUNDATION_EXTERN const struct %s {' % resources_constant_name
      h_file.section do |struct|
        enumerate_sections do |section_key, enumerate_keys|
          struct.writeln 'struct %s {' % section_key
          struct.section do |section_struct|
            enumerate_keys.call do |key, value, comment|
              if documented?
                section_struct.writeln '/// %s' % (comment || value) #unless comment.nil?
              end
              section_struct.writeln '__unsafe_unretained NSString *%s;' % key
            end
          end
          struct.writeln '} %s;' % section_key
        end
      end
      h_file.writeln '} %s;' % resources_constant_name
    end

    def build_impl_contents m_file
      m_file.writeln BANNER
      m_file.writeln
      m_file.writeln '#import "R.h"'
      m_file.writeln
      m_file.writeln 'const struct %s %s = {' % [resources_constant_name, resources_constant_name]
      m_file.section do |struct|
        enumerate_sections do |section_key, enumerate_keys|
          struct.writeln '.%s = {' % section_key
          struct.section do |section_struct|
            enumerate_keys.call do |key, value|
              section_struct.writeln '.%s = @"%s",' % [key, value]
            end
          end
          struct.writeln '},'
        end
      end
      m_file.writeln '};'
    end

    def build_swift_contents swift_file
      swift_file.writeln BANNER
      swift_file.writeln 'public class %s {' % resources_constant_name
      swift_file.section do |struct|
        enumerate_sections do |section_key, enumerate_keys|
          struct.writeln 'public enum %s: String {' % section_key
          struct.section do |section_struct|
            enumerate_keys.call do |key, value, comment|
              if documented?
                section_struct.writeln '/// %s' % (comment || value) #unless comment.nil?
              end
              section_struct.writeln 'case %s = "%s"' % [key, value]
            end
          end
          struct.writeln '}'
        end
      end
      swift_file.writeln '}'
      swift_file.writeln
      swift_file.writeln SWIFT_EXTENSIONS % resources_constant_name
    end

    def enumerate_sections
      # Iterate sections ordered by key
      for section_key, section_content in @sections.sort
        next if section_content.length == 0

        # Pass section key and block to yield the keys ordered
        proc = Proc.new do |&block|
          for key, value in section_content.sort
            if value.is_a? Hash
              block.call key, value[:value], value[:comment]
            else
              block.call key, value, nil
            end
          end
        end
        yield section_key, proc
      end
    end

end