Skip to content

Instantly share code, notes, and snippets.

@colinta
Last active August 29, 2015 14:02
Show Gist options
  • Save colinta/212dc1858bf86c05c5b1 to your computer and use it in GitHub Desktop.
Save colinta/212dc1858bf86c05c5b1 to your computer and use it in GitHub Desktop.
Skip to `generate.rb` to see the good stuff, and the output follows.
class AndroidTag
attr_accessor :is_root
def initialize(tag, *args, &block)
@buffer = []
@attrs = {}
@raw_tag = tag.to_s
if rename = AndroidXml.tags[tag.to_s][:rename]
@tag = rename
else
@tag = tag.to_s.gsub('_', '-')
end
args.each do |arg|
if arg.is_a?(Hash)
@attrs.merge!(arg)
else
raise ArgumentError.new("Unknown argument #{arg.inspect} in #{self.class}#new")
end
end
@generate = block
end
def method_missing(method_name, *args, &block)
tag = AndroidTag.new(method_name, *args, &block)
@buffer << tag
tag
end
def attrs(whitespace)
attrs = {}
if is_root
attrs.merge!(AndroidXml.root_tag[:defaults])
end
attrs.merge!(AndroidXml.tags[@raw_tag][:defaults])
attrs.merge!(@attrs)
output = ''
is_first = true
attrs.each do |key, value|
next if value.nil?
key = key.to_s
if AndroidXml.tags[@tag][:attrs].key?(key)
xml_key = AndroidXml.tags[@tag][:attrs][key]
elsif key.to_s.include?(':')
xml_key = key.to_s
else
xml_key = "android:#{key}"
end
if is_first
output << " #{xml_key}=\"#{value}\""
is_first = false
else
output << "\n#{whitespace}#{xml_key}=\"#{value}\""
end
end
output
end
def generate(tab='')
whitespace = "#{tab} #{' ' * @tag.length}"
output = "#{tab}<#{@tag}#{attrs(whitespace)}"
if @generate
inside = generate_block(tab + AndroidXml.tab)
if !inside || inside.strip.empty?
output << " />\n"
else
output << ">"
if inside.lstrip.start_with?('<')
output << "\n" << inside << "#{tab}</#{@tag}>\n"
else
output << inside << "</#{@tag}>\n"
end
end
else
output <<" />\n"
end
output
end
def generate_block(tab='')
return @block_output if @block_output
output = ''
if @generate
text = instance_exec(&@generate)
@buffer.each do |tag|
output << tag.generate(tab)
end
if text.is_a?(String)
output << text
end
end
@block_output = output
end
def to_s
generate
end
def to_ary
[to_s]
end
def out
puts to_s
end
end
class AndroidXml < AndroidTag
ROOT = '__root_node_with_a_long_special_name__'
class << self
attr_writer :tab
def files
@files ||= []
end
def write_all
files.each do |xml_file|
File.open(xml_file.filename, 'w') do |f|
f.write(xml_file.to_s)
end
puts "✓ #{xml_file.filename}"
end
end
def output_all
files.each do |xml_file|
xml_file.out
end
end
def tags
@tags ||= Hash.new do |hash, key|
hash[key] = {
attrs: {},
defaults: {},
rename: nil,
}
end
end
def tab
@tab ||= ' '
end
def setup(&block)
instance_exec(&block)
end
def root_tag
tags[AndroidXml::ROOT]
end
def tag(name, &block)
context_was = @context
if name.is_a?(Hash)
@context = nil
name.each do |shortcut, tag_name|
raise "There can be only one key-value pair" if @context
@context = shortcut.to_s
tags[@context][:rename] = tag_name.to_s
end
else
@context = name.to_s
end
yield
@context = context_was
end
def rename(attrs)
if attrs.is_a?(Hash)
attrs.each do |attr_name, attr_rename|
tags[@context][:attrs][attr_name.to_s] = attr_rename.to_s
end
else
tags[@context][:attrs][attrs.to_s] = attrs.to_s
end
end
def defaults(attrs)
tags[@context][:defaults].merge!(attrs)
end
def root(&block)
tag(ROOT, &block)
end
end
attr :filename
def initialize(filename, &block)
@filename = filename
super(nil, &block)
AndroidXml.files << self
end
def method_missing(method_name, *args, &block)
raise "There can be only one (new: #{method_name}, old: #{@root})" if @root
@root = super
@root.is_root = true
@root
end
def generate
output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
output << "<!-- Do not edit this file. It was generated by AndroidXml -->\n"
output << generate_block
end
def out
puts "---- filename: #{@filename} ----"
super
puts "---------------#{'-' * @filename.to_s.length}-----"
end
end
require_relative './android_xml'
AndroidXml.setup do
# assign the xmlns to all root nodes
root do
defaults 'xmlns:android' => 'http://schemas.android.com/apk/res/android'
end
tag :resources do
# disable the xmlns attribute on the resource node
defaults 'xmlns:android' => nil
end
tag :string do
rename :name # suppress 'android:' prefix
end
tag :manifest do
rename :package
end
# creates a "tag shortcut"
tag :main_action => 'action' do
defaults name: 'android.intent.action.MAIN'
end
tag :launcher_category => 'category' do
defaults name: 'android.intent.category.LAUNCHER'
end
end
AndroidXml.new('AndroidManifest.xml') do
manifest package: 'com.colinta.MyFirstAndroidApp', versionCode: 1, versionName: 1.0 do
uses_sdk minSdkVersion: 11
application label: '@string/app_name', icon: '@drawable/ic_launcher' do
activity name: 'MainActivity', label: '@string/app_name' do
intent_filter do
main_action
launcher_category
end
end
activity name: 'DisplayMessageActivity', label: '@string/app_name', parentActivityName: 'com.colinta.MyFirstAndroidApp.MainActivity'
end
end
end
AndroidXml.new('res/values/strings.xml') do
resources do
string(name: 'app_name') { 'Harroo' }
string(name: 'hello') { 'Well, Harroo!' }
string(name: 'portrait') { 'portrait' }
string(name: 'landscape') { 'landscape' }
string(name: 'button_send') { 'Send' }
string(name: 'action_search') { 'Search' }
string(name: 'action_settings') { 'Settings' }
string(name: 'title_activity_main') { 'MainActivity' }
end
end
AndroidXml.new('res/menu/main_activity_actions.xml') do
menu do
item id: '@+id/action_search',
icon: '@drawable/ic_action_search',
title: '@string/action_search',
showAsAction: "ifRoom"
item id: '@+id/action_settings',
title: '@string/action_settings',
showAsAction: "never"
end
end
# AndroidXml.write_all
AndroidXml.output_all
---- filename: AndroidManifest.xml ----
<?xml version="1.0" encoding="utf-8"?>
<!-- Do not edit this file. It was generated by AndroidXml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.colinta.MyFirstAndroidApp"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="11" />
<application android:label="@string/app_name"
android:icon="@drawable/ic_launcher">
<activity android:name="MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="DisplayMessageActivity"
android:label="@string/app_name"
android:parentActivityName="com.colinta.MyFirstAndroidApp.MainActivity" />
</application>
</manifest>
---------------------------------------
---- filename: res/values/strings.xml ----
<?xml version="1.0" encoding="utf-8"?>
<!-- Do not edit this file. It was generated by AndroidXml -->
<resources>
<string name="app_name">Harroo</string>
<string name="hello">Well, Harroo!</string>
<string name="portrait">portrait</string>
<string name="landscape">landscape</string>
<string name="button_send">Send</string>
<string name="action_search">Search</string>
<string name="action_settings">Settings</string>
<string name="title_activity_main">MainActivity</string>
</resources>
------------------------------------------
---- filename: res/menu/main_activity_actions.xml ----
<?xml version="1.0" encoding="utf-8"?>
<!-- Do not edit this file. It was generated by AndroidXml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_search"
android:icon="@drawable/ic_action_search"
android:title="@string/action_search"
android:showAsAction="ifRoom" />
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:showAsAction="never" />
</menu>
------------------------------------------------------
@colinta
Copy link
Author

colinta commented Jun 18, 2014

And yes, this will probably be my first rm-android gem, once the beta is out :-)

@jamonholmgren
Copy link

Looks really good, Colin!

@colinta
Copy link
Author

colinta commented Jun 18, 2014

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment