Commit 4a0db3ff authored by Ivan Tyagov's avatar Ivan Tyagov

Use only one global RenderJs variable to access different modules instead of...

Use only one global RenderJs variable to access different modules instead of many global variable here and there.
parent 43c8d0b5
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
</item> </item>
<item> <item>
<key> <string>_EtagSupport__etag</string> </key> <key> <string>_EtagSupport__etag</string> </key>
<value> <string>ts37261763.42</string> </value> <value> <string>ts37323241.04</string> </value>
</item> </item>
<item> <item>
<key> <string>__name__</string> </key> <key> <string>__name__</string> </key>
...@@ -36,15 +36,192 @@ var is_ready;\n ...@@ -36,15 +36,192 @@ var is_ready;\n
is_ready = false; // XXX: dirty flag to be removed (indicates if ready event has been handled)\n is_ready = false; // XXX: dirty flag to be removed (indicates if ready event has been handled)\n
\n \n
\n \n
\n
/*\n /*\n
* Generic cache implementation that can fall back to local namespace storage\n * Javascript Gadget representation\n
* if no "modern" storage like localStorage is available\n */\n
*/ \n function Gadget(id, dom) {\n
var Cache = (function() {\n this.id = id;\n
this.dom = dom;\n
this.is_ready = false;\n
}\n
\n
Gadget.prototype.getId = function() {\n
return this.id;\n
};\n
\n
Gadget.prototype.getDom = function() {\n
return this.dom;\n
};\n
\n
Gadget.prototype.isReady = function() {\n
/*\n
* Return True if remote gadget is loaded into DOM.\n
*/\n
return this.is_ready;\n
};\n
\n
Gadget.prototype.setReady = function() {\n
/*\n
* Return True if remote gadget is loaded into DOM.\n
*/\n
this.is_ready = true;\n
};\n
\n
\n
Gadget.prototype.getParent = function() {\n
/*\n
* Get Gadget\'s parent by using DOM\n
*/\n
// XXX:\n
};\n
\n
\n
/*\n
* Generic Gadget library renderer\n
*/\n
\n
var RenderJs = (function () {\n
\n \n
return {\n return {\n
\n \n
bootstrap: function (root){\n
/* initial load application gadget */\n
RenderJs.loadGadgetFromUrl(root);\n
RenderJs.load(root);\n
},\n
\n
load: function (root) {\n
/* Load gadget layout by traversing DOM */\n
var gadget_list;\n
gadget_list = root.find("[gadget]");\n
// Load chilren\n
gadget_list.each(function() {\n
RenderJs.loadGadgetFromUrl($(this));\n
});\n
},\n
\n
updateAndRecurse: function(gadget, data){\n
/* Update current gadget and recurse down */\n
gadget.append(data);\n
// a gadget may contain sub gadgets\n
this.load(gadget);\n
},\n
\n
loadGadgetFromUrl: function(gadget) {\n
/* Load gadget\'s SPECs from URL */\n
var url, gadget_id, gadget_property, cacheable, cache_id, app_cache, data, gadget_js;\n
url = gadget.attr("gadget");\n
gadget_id = gadget.attr("id");\n
\n
// register gadget in javascript namespace\n
gadget_js = new Gadget(gadget_id, gadget);\n
RenderJs.GadgetIndex.registerGadget(gadget_js);\n
\n
if (url!==undefined && url!==""){\n
gadget_property = gadget.attr("gadget:property");\n
cacheable = false;\n
if (gadget_property!==undefined) {\n
gadget_property = $.parseJSON(gadget_property);\n
cacheable = Boolean(gadget_property.cacheable);\n
}\n
//cacheable = false ; // to develop faster\n
if (cacheable) {\n
// get from cache if possible, use last part from URL as cache_key\n
cache_id = gadget_property.cache_id;\n
app_cache = RenderJs.Cache.get(cache_id, undefined);\n
if(app_cache===undefined || app_cache===null){\n
// not in cache so we pull from network and cache\n
//console.log("not in cache: " + cache_id + " " + url);\n
$.ajax({url:url,\n
yourCustomData: {"gadget_id": gadget_id, "cache_id": cache_id},\n
success: function (data) {\n
cache_id = this.yourCustomData.cache_id;\n
gadget_id = this.yourCustomData.gadget_id;\n
//console.log("set in cache: " + cache_id);\n
RenderJs.Cache.set(cache_id, data);\n
RenderJs.GadgetIndex.getGadgetById(gadget_id).setReady();\n
RenderJs.updateAndRecurse(gadget, data);\n
RenderJs.checkAndTriggerReady();\n
}});\n
}\n
else {\n
// get from cache\n
data = app_cache;\n
gadget_js.setReady();\n
this.updateAndRecurse(gadget, data);\n
this.checkAndTriggerReady();\n
}\n
}\n
else {\n
// not to be cached\n
//console.log("Not to be cached " + url + gadget_id);\n
$.ajax({url:url,\n
yourCustomData: {"gadget_id": gadget_id},\n
success: function (data) {\n
gadget_id = this.yourCustomData.gadget_id;\n
RenderJs.GadgetIndex.getGadgetById(gadget_id).setReady();\n
RenderJs.updateAndRecurse(gadget, data);\n
RenderJs.checkAndTriggerReady();\n
}});\n
}\n
}\n
else {\n
// gadget is an inline one so no need to load it from network\n
gadget_js.setReady();\n
RenderJs.checkAndTriggerReady();\n
}\n
},\n
\n
checkAndTriggerReady: function() {\n
/*\n
* Trigger "ready" event only if all gadgets were marked as "ready"\n
*/\n
var is_gadget_list_loaded;\n
is_gadget_list_loaded = RenderJs.GadgetIndex.isGadgetListLoaded();\n
if (is_gadget_list_loaded){\n
if (!is_ready) {\n
//console.log("trigger");\n
RenderJs.GadgetIndex.getRootGadget().getDom().trigger("ready");\n
}\n
is_ready = true;\n
}\n
return is_gadget_list_loaded;\n
},\n
\n
update: function (root) {\n
/* update gadget with data from remote source */\n
root.find("[gadget]").each(function(i,v){RenderJs.updateGadgetData($(this));});\n
},\n
\n
updateGadgetWithDataHandler: function (result) {\n
var data_handler;\n
data_handler = this.yourCustomData.data_handler;\n
if (data_handler!==undefined){\n
eval(data_handler+ "(result)");\n
}\n
},\n
\n
updateGadgetData: function(gadget) {\n
/* Do real gagdet update here */\n
var data_source, data_handler;\n
data_source = gadget.attr("gadget:data-source");\n
data_handler = gadget.attr("gadget:data-handler");\n
// acquire data and pass it to method handler\n
if (data_source!==undefined){\n
$.ajax({url:data_source,\n
dataType: "json",\n
yourCustomData: {"data_handler": data_handler},\n
success: RenderJs.updateGadgetWithDataHandler});}\n
},\n
\n
Cache : (function() {\n
/*\n
* Generic cache implementation that can fall back to local namespace storage\n
* if no "modern" storage like localStorage is available\n
*/\n
\n
return {\n
\n
ROOT_CACHE_ID: \'APP_CACHE\',\n ROOT_CACHE_ID: \'APP_CACHE\',\n
\n \n
getCacheId: function (cache_id) {\n getCacheId: function (cache_id) {\n
...@@ -104,7 +281,7 @@ var Cache = (function() {\n ...@@ -104,7 +281,7 @@ var Cache = (function() {\n
$.jStorage.set(cache_id, data);\n $.jStorage.set(cache_id, data);\n
}\n }\n
}}()),\n }}()),\n
\n \n
NameSpaceStorageCachePlugin: (function() {\n NameSpaceStorageCachePlugin: (function() {\n
/*\n /*\n
* This plugin saves within current page namespace.\n * This plugin saves within current page namespace.\n
...@@ -125,13 +302,12 @@ var Cache = (function() {\n ...@@ -125,13 +302,12 @@ var Cache = (function() {\n
\n \n
}}())\n }}())\n
\n \n
}}());\n }}()),\n
\n \n
/*\n TabbularGadget : (function () {\n
/*\n
* Generic tabular gadget\n * Generic tabular gadget\n
*/\n */\n
var TabbularGadget = (function () {\n
\n
return {\n return {\n
\n \n
toggleVisibility: function(visible_dom) {\n toggleVisibility: function(visible_dom) {\n
...@@ -157,72 +333,30 @@ var TabbularGadget = (function () {\n ...@@ -157,72 +333,30 @@ var TabbularGadget = (function () {\n
\n \n
tab_container.append(html_string);\n tab_container.append(html_string);\n
tab_gadget = tab_container.find(".gadget");\n tab_gadget = tab_container.find(".gadget");\n
\n \n
// XXX: we should unregister all gadgets (if any we replace now in DOM)\n // XXX: we should unregister all gadgets (if any we replace now in DOM)\n
\n \n
// render new gadget\n // render new gadget\n
is_ready = false;\n is_ready = false;\n
RenderJs.loadGadgetFromUrl(tab_gadget);\n RenderJs.loadGadgetFromUrl(tab_gadget);\n
// clear previous events\n // clear previous events\n
GadgetIndex.getRootGadget().getDom().bind("ready", function (){\n RenderJs.GadgetIndex.getRootGadget().getDom().bind("ready", function (){\n
if (!is_ready){\n if (!is_ready){\n
RenderJs.updateGadgetData(tab_gadget);\n RenderJs.updateGadgetData(tab_gadget);\n
is_ready = true;\n is_ready = true;\n
}\n }\n
});\n });\n
}\n }\n
\n
}}());\n
\n
\n
\n
/*\n
* Javascript Gadget representation\n
*/\n
function Gadget(id, dom) {\n
this.id = id;\n
this.dom = dom;\n
this.is_ready = false;\n
}\n
\n
Gadget.prototype.getId = function() {\n
return this.id;\n
};\n
\n \n
Gadget.prototype.getDom = function() {\n }}()),\n
return this.dom;\n
};\n
\n
Gadget.prototype.isReady = function() {\n
/*\n
* Return True if remote gadget is loaded into DOM.\n
*/\n
return this.is_ready;\n
};\n
\n
Gadget.prototype.setReady = function() {\n
/*\n
* Return True if remote gadget is loaded into DOM.\n
*/\n
this.is_ready = true;\n
};\n
\n \n
\n \n
Gadget.prototype.getParent = function() {\n GadgetIndex : (function () {\n
/*\n /*\n
* Get Gadget\'s parent by using DOM\n
*/\n
// XXX:\n
};\n
\n
\n
/*\n
* Generic gadget index placeholder\n * Generic gadget index placeholder\n
*/\n */\n
var GadgetIndex = (function () {\n
\n
var gadget_list = [];\n var gadget_list = [];\n
\n \n
return {\n return {\n
\n \n
getGadgetList: function() {\n getGadgetList: function() {\n
...@@ -255,7 +389,7 @@ var GadgetIndex = (function () {\n ...@@ -255,7 +389,7 @@ var GadgetIndex = (function () {\n
*/\n */\n
var gadget;\n var gadget;\n
gadget = undefined;\n gadget = undefined;\n
$(GadgetIndex.getGadgetList()).each(\n $(RenderJs.GadgetIndex.getGadgetList()).each(\n
function (index, value) {\n function (index, value) {\n
if (value.getId()===gadget_id) {\n if (value.getId()===gadget_id) {\n
gadget = value;\n gadget = value;\n
...@@ -291,15 +425,13 @@ var GadgetIndex = (function () {\n ...@@ -291,15 +425,13 @@ var GadgetIndex = (function () {\n
*/\n */\n
// XXX:\n // XXX:\n
}\n }\n
\n
}}());\n
\n \n
}}()),\n
\n \n
/*\n InteractionGadget : (function () {\n
/*\n
* Basic gadget interaction gadget implementation.\n * Basic gadget interaction gadget implementation.\n
*/\n */\n
var InteractionGadget = (function () {\n
\n
return {\n return {\n
\n \n
bind: function (gadget_dom){\n bind: function (gadget_dom){\n
...@@ -313,24 +445,24 @@ var InteractionGadget = (function () {\n ...@@ -313,24 +445,24 @@ var InteractionGadget = (function () {\n
source = $(value).attr("source").split(".");\n source = $(value).attr("source").split(".");\n
source_gadget_id = source[0];\n source_gadget_id = source[0];\n
source_method_id = source[1];\n source_method_id = source[1];\n
source_gadget = GadgetIndex.getGadgetById(source_gadget_id);\n source_gadget = RenderJs.GadgetIndex.getGadgetById(source_gadget_id);\n
\n \n
destination = $(value).attr("destination").split(".");\n destination = $(value).attr("destination").split(".");\n
destination_gadget_id = destination[0];\n destination_gadget_id = destination[0];\n
destination_method_id = destination[1];\n destination_method_id = destination[1];\n
destination_gadget = GadgetIndex.getGadgetById(destination_gadget_id);\n destination_gadget = RenderJs.GadgetIndex.getGadgetById(destination_gadget_id);\n
\n \n
if (source_gadget.hasOwnProperty(source_method_id)){\n if (source_gadget.hasOwnProperty(source_method_id)){\n
// direct javascript use case\n // direct javascript use case\n
func_body = \'GadgetIndex.getGadgetById("\' + source_gadget_id + \'")["original_\' + source_method_id + \'"]();\';\n func_body = \'RenderJs.GadgetIndex.getGadgetById("\' + source_gadget_id + \'")["original_\' + source_method_id + \'"]();\';\n
func_body = func_body + \'\\nGadgetIndex.getGadgetById("\' + destination_gadget_id + \'")["\' + destination_method_id + \'"]();\';\n func_body = func_body + \'\\nRenderJs.GadgetIndex.getGadgetById("\' + destination_gadget_id + \'")["\' + destination_method_id + \'"]();\';\n
func = new Function(func_body);\n func = new Function(func_body);\n
source_gadget["original_" + source_method_id] = source_gadget[source_method_id];\n source_gadget["original_" + source_method_id] = source_gadget[source_method_id];\n
source_gadget[source_method_id] = func;\n source_gadget[source_method_id] = func;\n
}\n }\n
else{\n else{\n
// this is a custom event attached to HTML gadget representation\n // this is a custom event attached to HTML gadget representation\n
func_body = \'GadgetIndex.getGadgetById("\' + destination_gadget_id + \'")["\' + destination_method_id + \'"]();\';\n func_body = \'RenderJs.GadgetIndex.getGadgetById("\' + destination_gadget_id + \'")["\' + destination_method_id + \'"]();\';\n
func = new Function(func_body);\n func = new Function(func_body);\n
source_gadget.dom.bind(source_method_id, func);\n source_gadget.dom.bind(source_method_id, func);\n
}\n }\n
...@@ -338,146 +470,7 @@ var InteractionGadget = (function () {\n ...@@ -338,146 +470,7 @@ var InteractionGadget = (function () {\n
}\n }\n
);\n );\n
}\n }\n
}}());\n }}())\n
\n
\n
/*\n
* Generic Gadget library renderer\n
*/\n
\n
var RenderJs = (function () {\n
\n
return {\n
\n
bootstrap: function (root){\n
/* initial load application gadget */\n
RenderJs.loadGadgetFromUrl(root);\n
RenderJs.load(root);\n
},\n
\n
load: function (root) {\n
/* Load gadget layout by traversing DOM */\n
var gadget_list;\n
gadget_list = root.find("[gadget]");\n
// Load chilren\n
gadget_list.each(function() {\n
RenderJs.loadGadgetFromUrl($(this));\n
});\n
},\n
\n
updateAndRecurse: function(gadget, data){\n
/* Update current gadget and recurse down */\n
gadget.append(data);\n
// a gadget may contain sub gadgets\n
this.load(gadget);\n
},\n
\n
loadGadgetFromUrl: function(gadget) {\n
/* Load gadget\'s SPECs from URL */\n
var url, gadget_id, gadget_property, cacheable, cache_id, app_cache, data, gadget_js;\n
url = gadget.attr("gadget");\n
gadget_id = gadget.attr("id");\n
\n
// register gadget in javascript namespace\n
gadget_js = new Gadget(gadget_id, gadget);\n
GadgetIndex.registerGadget(gadget_js);\n
\n
if (url!==undefined && url!==""){\n
gadget_property = gadget.attr("gadget:property");\n
cacheable = false;\n
if (gadget_property!==undefined) {\n
gadget_property = $.parseJSON(gadget_property);\n
cacheable = Boolean(gadget_property.cacheable);\n
}\n
//cacheable = false ; // to develop faster\n
if (cacheable) {\n
// get from cache if possible, use last part from URL as cache_key\n
cache_id = gadget_property.cache_id;\n
app_cache = Cache.get(cache_id, undefined);\n
if(app_cache===undefined || app_cache===null){\n
// not in cache so we pull from network and cache\n
//console.log("not in cache: " + cache_id + " " + url);\n
$.ajax({url:url,\n
yourCustomData: {"gadget_id": gadget_id, "cache_id": cache_id},\n
success: function (data) {\n
cache_id = this.yourCustomData.cache_id;\n
gadget_id = this.yourCustomData.gadget_id;\n
//console.log("set in cache: " + cache_id);\n
Cache.set(cache_id, data);\n
GadgetIndex.getGadgetById(gadget_id).setReady();\n
RenderJs.updateAndRecurse(gadget, data);\n
RenderJs.checkAndTriggerReady();\n
}});\n
}\n
else {\n
// get from cache\n
data = app_cache;\n
gadget_js.setReady();\n
this.updateAndRecurse(gadget, data);\n
this.checkAndTriggerReady();\n
}\n
}\n
else {\n
// not to be cached\n
//console.log("Not to be cached " + url + gadget_id);\n
$.ajax({url:url,\n
yourCustomData: {"gadget_id": gadget_id},\n
success: function (data) {\n
gadget_id = this.yourCustomData.gadget_id;\n
GadgetIndex.getGadgetById(gadget_id).setReady();\n
RenderJs.updateAndRecurse(gadget, data);\n
RenderJs.checkAndTriggerReady();\n
}});\n
}\n
}\n
else {\n
// gadget is an inline one so no need to load it from network\n
gadget_js.setReady();\n
RenderJs.checkAndTriggerReady();\n
}\n
},\n
\n
checkAndTriggerReady: function() {\n
/*\n
* Trigger "ready" event only if all gadgets were marked as "ready"\n
*/\n
var is_gadget_list_loaded;\n
is_gadget_list_loaded = GadgetIndex.isGadgetListLoaded();\n
if (is_gadget_list_loaded){\n
if (!is_ready) {\n
//console.log("trigger");\n
GadgetIndex.getRootGadget().getDom().trigger("ready");\n
}\n
is_ready = true;\n
}\n
return is_gadget_list_loaded;\n
},\n
\n
update: function (root) {\n
/* update gadget with data from remote source */\n
root.find("[gadget]").each(function(i,v){RenderJs.updateGadgetData($(this));});\n
},\n
\n
updateGadgetWithDataHandler: function (result) {\n
var data_handler;\n
data_handler = this.yourCustomData.data_handler;\n
if (data_handler!==undefined){\n
eval(data_handler+ "(result)");\n
}\n
},\n
\n
updateGadgetData: function(gadget) {\n
/* Do real gagdet update here */\n
var data_source, data_handler;\n
data_source = gadget.attr("gadget:data-source");\n
data_handler = gadget.attr("gadget:data-handler");\n
// acquire data and pass it to method handler\n
if (data_source!==undefined){\n
$.ajax({url:data_source,\n
dataType: "json",\n
yourCustomData: {"data_handler": data_handler},\n
success: RenderJs.updateGadgetWithDataHandler});}\n
}\n
\n \n
}}()); }}());
...@@ -489,7 +482,7 @@ var RenderJs = (function () {\n ...@@ -489,7 +482,7 @@ var RenderJs = (function () {\n
</item> </item>
<item> <item>
<key> <string>size</string> </key> <key> <string>size</string> </key>
<value> <int>15882</int> </value> <value> <int>18703</int> </value>
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>_EtagSupport__etag</string> </key> <key> <string>_EtagSupport__etag</string> </key>
<value> <string>ts37256303.27</string> </value> <value> <string>ts37322419.16</string> </value>
</item> </item>
<item> <item>
<key> <string>__name__</string> </key> <key> <string>__name__</string> </key>
...@@ -30,40 +30,40 @@ function setupRenderJSTest(){\n ...@@ -30,40 +30,40 @@ function setupRenderJSTest(){\n
test(\'Cache\', function(){\n test(\'Cache\', function(){\n
cache_id = \'my_test\';\n cache_id = \'my_test\';\n
data = {\'gg\':1};\n data = {\'gg\':1};\n
Cache.set(cache_id, data);\n RenderJs.Cache.set(cache_id, data);\n
deepEqual(data, Cache.get(cache_id));\n deepEqual(data, RenderJs.Cache.get(cache_id));\n
});\n });\n
\n \n
\n \n
module("TabularGadget");\n module("TabularGadget");\n
test(\'addNewTabGadget\', function(){\n test(\'addNewTabGadget\', function(){\n
TabbularGadget.addNewTabGadget("qunit-fixture", "Person_view/Form_asRenderJSGadget", "ERP5Form.update", "Form_asJSON?form_id=Person_view");\n RenderJs.TabbularGadget.addNewTabGadget("qunit-fixture", "Person_view/Form_asRenderJSGadget", "ERP5Form.update", "Form_asJSON?form_id=Person_view");\n
equal($("#qunit-fixture").children(".gadget").length, 1);\n equal($("#qunit-fixture").children(".gadget").length, 1);\n
equal(GadgetIndex.getGadgetList().length, 1);\n equal(RenderJs.GadgetIndex.getGadgetList().length, 1);\n
\n \n
});\n });\n
\n \n
module("GadgetIndex");\n module("GadgetIndex");\n
test(\'GadgetIndex\', function(){\n test(\'GadgetIndex\', function(){\n
// re-init GadgetIndex\n // re-init GadgetIndex\n
$.each(GadgetIndex.getGadgetList(), function () {\n $.each(RenderJs.GadgetIndex.getGadgetList(), function () {\n
GadgetIndex.unregisterGadget(this);\n RenderJs.GadgetIndex.unregisterGadget(this);\n
});\n });\n
\n \n
$("#qunit-fixture").append(\'<div gadget="" id="new">XXXXXXXXXXXX</div>\');\n $("#qunit-fixture").append(\'<div gadget="" id="new">XXXXXXXXXXXX</div>\');\n
RenderJs.bootstrap($("#qunit-fixture"));\n RenderJs.bootstrap($("#qunit-fixture"));\n
GadgetIndex.getRootGadget().getDom().one("ready", function (){\n RenderJs.GadgetIndex.getRootGadget().getDom().one("ready", function (){\n
RenderJs.update($("#qunit-fixture"));\n RenderJs.update($("#qunit-fixture"));\n
});\n });\n
equal(GadgetIndex.getGadgetList().length, 2);\n equal(RenderJs.GadgetIndex.getGadgetList().length, 2);\n
equal(true, GadgetIndex.isGadgetListLoaded());\n equal(true, RenderJs.GadgetIndex.isGadgetListLoaded());\n
equal($("#qunit-fixture").attr("id"), GadgetIndex.getRootGadget().getDom().attr("id"));\n equal($("#qunit-fixture").attr("id"), RenderJs.GadgetIndex.getRootGadget().getDom().attr("id"));\n
equal(GadgetIndex.getGadgetById("qunit-fixture"), GadgetIndex.getRootGadget());\n equal(RenderJs.GadgetIndex.getGadgetById("qunit-fixture"), RenderJs.GadgetIndex.getRootGadget());\n
\n \n
// unregister gadget\n // unregister gadget\n
GadgetIndex.unregisterGadget(GadgetIndex.getGadgetById("qunit-fixture"));\n RenderJs.GadgetIndex.unregisterGadget(RenderJs.GadgetIndex.getGadgetById("qunit-fixture"));\n
equal(GadgetIndex.getGadgetList().length, 1);\n equal(RenderJs.GadgetIndex.getGadgetList().length, 1);\n
equal(GadgetIndex.getGadgetById("new"), GadgetIndex.getRootGadget());\n equal(RenderJs.GadgetIndex.getGadgetById("new"), RenderJs.GadgetIndex.getRootGadget());\n
\n \n
\n \n
});\n });\n
...@@ -80,7 +80,7 @@ function setupRenderJSTest(){\n ...@@ -80,7 +80,7 @@ function setupRenderJSTest(){\n
</item> </item>
<item> <item>
<key> <string>size</string> </key> <key> <string>size</string> </key>
<value> <int>1809</int> </value> <value> <int>1962</int> </value>
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
......
17 18
\ No newline at end of file \ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment