Titanium
Alloy

Alloy で AttributedString を使えるようにする

More than 3 years have passed since last update.

Titanium SDK 3.2.0.GA から追加された AttributedString を Alloy のビューとスタイルのみで使えるようにする、Alloy 自体の改造になります。

Titanium SDK 3.6 系から AttributedString が Android でもサポートされ、ネームスペースも Ti.UI.iOS.AttributedString から Ti.UI.AttributedString へ変更されるため、Alloy 自体の対応は 1.6 系からとなるようです(待てません。というか、諸事情で SDK と Alloy のバージョンを動かせない場合もありますし)。

そもそもコントローラに Ti.UI.iOS.createAttributedStinrg を直書きすれば良いのですが、Alloy を使っていて今更そんなこともしたくないですし。。。

というわけで、Alloy のコンパイラに手を入れて、ビューとスタイルのみで AttributedString を使えるようにしたいというわけです。実際にこれを適用すると、ビューとスタイルは以下のように記述できるようになります。

index.xml

<Alloy>

<Window>
<Label>
<AttributedString class="as">
Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
</AttributedString>
</Label>
<TextField>
<AttributedString class="as">
Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
</AttributedString>
<AttributedHintText class="as">
Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
</AttributedHintText>
</TextField>
<TextArea>
<AttributedString class="as">
Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
</AttributedString>
</TextArea>
</Window>
</Alloy>

index.tss

"Window": {

backgroundColor: '#fff',
layout: 'vertical'
},
"Label": {
top: 20,
right: 10,
left: 10,
width: Ti.UI.FILL,
height: Ti.UI.SIZE
},
"TextField": {
top: 20,
right: 10,
left: 10,
width: Ti.UI.FILL,
height: 44
},
"TextArea": {
top: 20,
right: 10,
left: 10,
width: Ti.UI.FILL,
height: 200
},
".as": {
attributes: [
{
type: Ti.UI.ATTRIBUTE_UNDERLINES_STYLE,
value: Ti.UI.ATTRIBUTE_UNDERLINE_STYLE_SINGLE,
range: [0, 107]
},
{
type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
value: "red",
range: [18, 12]
},
{
type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
value: "blue",
range: [31, 2]
},
{
type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
value: "yellow",
range: [40, 6]
},
{
type: Ti.UI.ATTRIBUTE_FOREGROUND_COLOR,
value: "orange",
range: [0, 107]
},
{
type: Ti.UI.ATTRIBUTE_FOREGROUND_COLOR,
value: "black",
range: [40, 6]
}
]
}

それではインストールされた Alloy のフォルダへ移動して、以下の改造を適用していきます。

alloy/Alloy/common/constants.js

186 行目にある AdView の下辺りに、以下の 1 行を挿入します。

AttributedString: NS_TI_UI_IOS,

alloy/Alloy/commands/compile/parsers/Ti.UI.iOS.AttributedString

AttributedString 用に以下のコードを新規に設置します。

var _ = require('../../../lib/alloy/underscore'),

styler = require('../styler'),
U = require('../../../utils'),
CU = require('../compilerUtils'),
tiapp = require('../../../tiapp');

var MIN_VERSION = '3.2.0';

exports.parse = function(node, state) {
return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
if (tiapp.version.lt(tiapp.getSdkVersion(), MIN_VERSION)) {
U.die('Ti.UI.iOS.AttributedString (line ' + node.lineNumber + ') requires Titanium 3.2.0+');
}

// Get label text from node text, if present
var nodeText = U.XML.getNodeText(node);
if (nodeText) {
if (U.isLocaleAlias(nodeText)) {
state.extraStyle = {'text': styler.STYLE_EXPR_PREFIX + nodeText};
} else {
state.extraStyle = styler.createVariableStyle('text', "'" + U.trim(nodeText.replace(/'/g, "\\'")) + "'");
}

if (nodeText.match(/\{([^}]+)\}/) !== null) {
state.extraStyle = nodeText;
}
}

var nodeState = require('./default').parse(node, state);
delete state.extraStyle;

var code = nodeState.code;

return {
parent: {
node: node,
symbol: args.symbol
},
code: code
};
}

alloy/Alloy/commands/compile/parsers/Ti.UI.Label

Ti.UI.LabelAttributedString が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,

styler = require('../styler'),
CU = require('../compilerUtils'),
U = require('../../../utils');

exports.parse = function(node, state) {
return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
var attributedStringsymbol,
attributedStringObj = {},
code = '';

_.each(U.XML.getElementsFromNodes(node.childNodes), function(child){
if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
child.setAttribute('ns', 'Ti.UI.iOS');
}

if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
code += CU.generateNodeExtended(child, state, {
parent: {},
post: function(node, state, args) {
attributedStringsymbol = state.parent.symbol;
}
});

node.removeChild(child);
}
});

if (attributedStringsymbol) {
attributedStringObj = styler.createVariableStyle('attributedString', attributedStringsymbol);
}

// Get label text from node text, if present
var nodeText = U.XML.getNodeText(node),
textObj = {};
if (nodeText) {
if (U.isLocaleAlias(nodeText)) {
textObj = {'text': styler.STYLE_EXPR_PREFIX + nodeText};
} else {
textObj = styler.createVariableStyle('text', "'" + U.trim(nodeText.replace(/'/g, "\\'")) + "'");
}

if (nodeText.match(/\{([^}]+)\}/) !== null) {
textObj["text"] = nodeText;
}
}

state.extraStyle = _.extend(attributedStringObj, textObj);

var nodeState = require('./default').parse(node, state);
code += nodeState.code;

// Generate runtime code using default
return _.extend(nodeState, {
code: code
});
}

alloy/Alloy/commands/compile/parsers/Ti.UI.TextField

Ti.UI.TextFieldAttributedStringAttributedHintText が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,

styler = require('../styler'),
U = require('../../../utils'),
CU = require('../compilerUtils'),
CONST = require('../../../common/constants');

var KEYBOARD_TYPES = [
'DEFAULT', 'ASCII', 'NUMBERS_PUNCTUATION', 'URL', 'EMAIL', 'DECIMAL_PAD', 'NAMEPHONE_PAD',
'NUMBER_PAD', 'PHONE_PAD'
];
var RETURN_KEY_TYPES = [
'DEFAULT', 'DONE', 'EMERGENCY_CALL', 'GO', 'GOOGLE', 'JOIN', 'NEXT', 'ROUTE',
'SEARCH', 'SEND', 'YAHOO'
];
var AUTOCAPITALIZATION_TYPES = [
'ALL', 'NONE', 'SENTENCES', 'WORDS'
];

exports.parse = function(node, state) {
return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
var code = '',
postCode = '',
extras = [],
proxyProperties = {};

// iterate through all children of the TextField
_.each(U.XML.getElementsFromNodes(node.childNodes), function(child) {
var fullname = CU.getNodeFullname(child),
isProxyProperty = false,
isControllerNode = false,
isAttributedString = false,
isAttributedHintText = false,
hasUiNodes = false,
controllerSymbol,
parentSymbol;

if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
child.setAttribute('ns', 'Ti.UI.iOS');
}

if (child.nodeName === 'AttributedHintText' && !child.hasAttribute('ns')) {
child.nodeName = 'AttributedString';
child.setAttribute('ns', 'Ti.UI.iOS');
isAttributedHintText = true;
}

// validate the child element and determine if it's part of
// the textfield or a proxy property assigment
if (!CU.isNodeForCurrentPlatform(child)) {
return;
} else if (_.contains(CONST.CONTROLLER_NODES, fullname)) {
isControllerNode = true;
} else if (fullname.split('.')[0] === '_ProxyProperty') {
isProxyProperty = true;
} else if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
if (!isAttributedHintText) {
isAttributedString = true;
}
}

// generate the node
code += CU.generateNodeExtended(child, state, {
parent: {},
post: function(node, state, args) {
controllerSymbol = state.controller;
parentSymbol = state.parent ? state.parent.symbol : state.item.symbol;
}
});

// manually handle controller node proxy properties
if (isControllerNode) {

// set up any proxy properties at the top-level of the controller
var inspect = CU.inspectRequireNode(child);
_.each(_.uniq(inspect.names), function(name) {
if (name.split('.')[0] === '_ProxyProperty') {
var prop = U.proxyPropertyNameFromFullname(name);
proxyProperties[prop] = controllerSymbol + '.getProxyPropertyEx("' + prop + '", {recurse:true})';
} else {
hasUiNodes = true;
}
});
}

// generate code for proxy property assignments
if (isProxyProperty) {
proxyProperties[U.proxyPropertyNameFromFullname(fullname)] = parentSymbol;

// generate code for the attribtuedString
} else if (isAttributedString) {
proxyProperties.attributedString = parentSymbol;
node.removeChild(child);

// generate code for the attribtuedHintText
} else if (isAttributedHintText) {
proxyProperties.attributedHintText = parentSymbol;
node.removeChild(child);

// generate code for the child components
} else if (hasUiNodes || !isControllerNode) {
postCode += '<%= parentSymbol %>.add(' + parentSymbol + ');';
}

});

// support shortcuts for keyboard type, return key type, and autocapitalization
var keyboardType = node.getAttribute('keyboardType');
if (_.contains(KEYBOARD_TYPES, keyboardType.toUpperCase())) {
node.setAttribute('keyboardType', 'Ti.UI.KEYBOARD_' + keyboardType.toUpperCase());
}
var returnKey = node.getAttribute('returnKeyType');
if (_.contains(RETURN_KEY_TYPES, returnKey.toUpperCase())) {
node.setAttribute('returnKeyType', 'Ti.UI.RETURNKEY_' + returnKey.toUpperCase());
}
var autocapitalization = node.getAttribute('keyboardType');
if (_.contains(AUTOCAPITALIZATION_TYPES, autocapitalization.toUpperCase())) {
node.setAttribute('autocapitalization', 'Ti.UI.TEXT_AUTOCAPITALIZATION_' + autocapitalization.toUpperCase());
}

// add all creation time properties to the state
if (node.hasAttribute('clearOnEdit')) {
var attr = node.getAttribute('clearOnEdit');
extras.push(['clearOnEdit', attr === 'true']);
}
_.each(proxyProperties, function(v, k) {
extras.push([k, v]);
});
if (extras.length) { state.extraStyle = styler.createVariableStyle(extras); }

// generate the code for the textfield itself
var nodeState = require('./default').parse(node, state);
code += nodeState.code;

// add the rows to the section
if (nodeState.parent && postCode) {
code += _.template(postCode, {
parentSymbol: nodeState.parent.symbol
});
}

// Update the parsing state
return _.extend(state, {
parent: nodeState.parent,
code: code
});
}

alloy/Alloy/commands/compile/parsers/Ti.UI.TextArea

Ti.UI.TextFieldAttributedString が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,

styler = require('../styler'),
CU = require('../compilerUtils'),
U = require('../../../utils'),
CONST = require('../../../common/constants');

var KEYBOARD_TYPES = [
'DEFAULT', 'ASCII', 'NUMBERS_PUNCTUATION', 'URL', 'EMAIL', 'DECIMAL_PAD', 'NAMEPHONE_PAD',
'NUMBER_PAD', 'PHONE_PAD'
];
var RETURN_KEY_TYPES = [
'DEFAULT', 'DONE', 'EMERGENCY_CALL', 'GO', 'GOOGLE', 'JOIN', 'NEXT', 'ROUTE',
'SEARCH', 'SEND', 'YAHOO'
];
var AUTOCAPITALIZATION_TYPES = [
'ALL', 'NONE', 'SENTENCES', 'WORDS'
];

exports.parse = function(node, state) {
return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
var code = '',
postCode = '',
extras = [],
proxyProperties = {};

// iterate through all children of the TextArea
_.each(U.XML.getElementsFromNodes(node.childNodes), function(child) {
var fullname = CU.getNodeFullname(child),
isProxyProperty = false,
isControllerNode = false,
isAttributedString = false,
hasUiNodes = false,
controllerSymbol,
parentSymbol;

if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
child.setAttribute('ns', 'Ti.UI.iOS');
}

// validate the child element and determine if it's part of
// the textarea or a proxy property assigment
if (!CU.isNodeForCurrentPlatform(child)) {
return;
} else if (_.contains(CONST.CONTROLLER_NODES, fullname)) {
isControllerNode = true;
} else if (fullname.split('.')[0] === '_ProxyProperty') {
isProxyProperty = true;
} else if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
isAttributedString = true;
}

// generate the node
code += CU.generateNodeExtended(child, state, {
parent: {},
post: function(node, state, args) {
controllerSymbol = state.controller;
parentSymbol = state.parent ? state.parent.symbol : null;
}
});

// manually handle controller node proxy properties
if (isControllerNode) {

// set up any proxy properties at the top-level of the controller
var inspect = CU.inspectRequireNode(child);
_.each(_.uniq(inspect.names), function(name) {
if (name.split('.')[0] === '_ProxyProperty') {
var prop = U.proxyPropertyNameFromFullname(name);
proxyProperties[prop] = controllerSymbol + '.getProxyPropertyEx("' + prop + '", {recurse:true})';
} else {
hasUiNodes = true;
}
});
}

// generate code for proxy property assignments
if (isProxyProperty) {
proxyProperties[U.proxyPropertyNameFromFullname(fullname)] = parentSymbol;

// generate code for the attribtuedString
} else if (isAttributedString) {
proxyProperties.attributedString = parentSymbol;
node.removeChild(child);

// generate code for the child components
} else if (hasUiNodes || !isControllerNode) {
postCode += '<%= parentSymbol %>.add(' + parentSymbol + ');';
}

});

// support shortcuts for keyboard type, return key type, and autocapitalization
var keyboardType = node.getAttribute('keyboardType');
if (_.contains(KEYBOARD_TYPES, keyboardType.toUpperCase())) {
node.setAttribute('keyboardType', 'Ti.UI.KEYBOARD_' + keyboardType.toUpperCase());
}
var returnKey = node.getAttribute('returnKeyType');
if (_.contains(RETURN_KEY_TYPES, returnKey.toUpperCase())) {
node.setAttribute('returnKeyType', 'Ti.UI.RETURNKEY_' + returnKey.toUpperCase());
}
var autocapitalization = node.getAttribute('keyboardType');
if (_.contains(AUTOCAPITALIZATION_TYPES, autocapitalization.toUpperCase())) {
node.setAttribute('autocapitalization', 'Ti.UI.TEXT_AUTOCAPITALIZATION_' + autocapitalization.toUpperCase());
}

// add all creation time properties to the state
if (node.hasAttribute('clearOnEdit')) {
var attr = node.getAttribute('clearOnEdit');
extras.push(['clearOnEdit', attr === 'true']);
}
_.each(proxyProperties, function(v, k) {
extras.push([k, v]);
});
if (extras.length) { state.extraStyle = styler.createVariableStyle(extras); }

// generate the code for the textarea itself
var nodeState = require('./default').parse(node, state);
code += nodeState.code;

// add the rows to the section
if (nodeState.parent && postCode) {
code += _.template(postCode, {
parentSymbol: nodeState.parent.symbol
});
}

// Update the parsing state
return _.extend(state, {
parent: nodeState.parent,
code: code
});
}

以上になります。特に、Ti.UI.TextField で hintText の色を変えたいとか、結構需要があったりしますよね。伝統芸能な、上にラベルをおいて change イベントを拾って。。。という実装もあるかと思いますが、attributedHintText を使ったほうが断然楽です。

注意事項

これは Alloy のコンパイラ自体に手を入れるため、自己責任でお願いします。ぼくの環境では問題なくコンパイルされますが、最悪 Alloy が壊れる可能性もあります。もちろん、Alloy 1.6 系で AttributedString がサポートされればこの改造は不要になります。