aboutsummaryrefslogtreecommitdiff
path: root/xorg-server/config
diff options
context:
space:
mode:
authormarha <marha@users.sourceforge.net>2010-06-11 14:16:16 +0000
committermarha <marha@users.sourceforge.net>2010-06-11 14:16:16 +0000
commitd1e4f4b8546c7955c66dd023bfd6ef437db9d21d (patch)
tree529985e77bfc95aa95fe5b540e8f42b0ef041206 /xorg-server/config
parent13919cf85a6ca41d97238de13344aba59e0f7680 (diff)
parent4c61bf84b11e26e6f22648668c95ea760a379163 (diff)
downloadvcxsrv-d1e4f4b8546c7955c66dd023bfd6ef437db9d21d.tar.gz
vcxsrv-d1e4f4b8546c7955c66dd023bfd6ef437db9d21d.tar.bz2
vcxsrv-d1e4f4b8546c7955c66dd023bfd6ef437db9d21d.zip
svn merge ^/branches/released .
Diffstat (limited to 'xorg-server/config')
-rw-r--r--xorg-server/config/Makefile.am78
-rw-r--r--xorg-server/config/dbus.c7
-rw-r--r--xorg-server/config/fdi2iclass.py202
-rw-r--r--xorg-server/config/hal.c80
-rw-r--r--xorg-server/config/udev.c62
5 files changed, 348 insertions, 81 deletions
diff --git a/xorg-server/config/Makefile.am b/xorg-server/config/Makefile.am
index 675a3b260..d8a7b45f6 100644
--- a/xorg-server/config/Makefile.am
+++ b/xorg-server/config/Makefile.am
@@ -1,39 +1,39 @@
-AM_CFLAGS = $(DIX_CFLAGS)
-
-noinst_LTLIBRARIES = libconfig.la
-libconfig_la_SOURCES = config.c config-backends.h
-
-if CONFIG_UDEV
-
-AM_CFLAGS += $(UDEV_CFLAGS)
-libconfig_la_SOURCES += udev.c
-libconfig_la_LIBADD = $(UDEV_LIBS)
-
-xorgconfddir = $(datadir)/X11/$(XF86CONFIGDIR)
-xorgconfd_DATA = 10-evdev.conf
-
-else
-
-if CONFIG_NEED_DBUS
-AM_CFLAGS += $(DBUS_CFLAGS)
-libconfig_la_SOURCES += dbus-core.c
-libconfig_la_LIBADD = $(DBUS_LIBS)
-
-if CONFIG_DBUS_API
-dbusconfigdir = $(sysconfdir)/dbus-1/system.d
-dbusconfig_DATA = xorg-server.conf
-
-libconfig_la_SOURCES += dbus.c
-endif
-
-if CONFIG_HAL
-AM_CFLAGS += $(HAL_CFLAGS)
-libconfig_la_SOURCES += hal.c
-libconfig_la_LIBADD += $(HAL_LIBS)
-endif
-
-endif # CONFIG_NEED_DBUS
-
-endif # !CONFIG_UDEV
-
-EXTRA_DIST = xorg-server.conf x11-input.fdi 10-evdev.conf
+AM_CFLAGS = $(DIX_CFLAGS)
+
+noinst_LTLIBRARIES = libconfig.la
+libconfig_la_SOURCES = config.c config-backends.h
+
+if CONFIG_UDEV
+
+AM_CFLAGS += $(UDEV_CFLAGS)
+libconfig_la_SOURCES += udev.c
+libconfig_la_LIBADD = $(UDEV_LIBS)
+
+xorgconfddir = $(datadir)/X11/$(XF86CONFIGDIR)
+xorgconfd_DATA = 10-evdev.conf
+
+else
+
+if CONFIG_NEED_DBUS
+AM_CFLAGS += $(DBUS_CFLAGS)
+libconfig_la_SOURCES += dbus-core.c
+libconfig_la_LIBADD = $(DBUS_LIBS)
+
+if CONFIG_DBUS_API
+dbusconfigdir = $(sysconfdir)/dbus-1/system.d
+dbusconfig_DATA = xorg-server.conf
+
+libconfig_la_SOURCES += dbus.c
+endif
+
+if CONFIG_HAL
+AM_CFLAGS += $(HAL_CFLAGS)
+libconfig_la_SOURCES += hal.c
+libconfig_la_LIBADD += $(HAL_LIBS)
+endif
+
+endif # CONFIG_NEED_DBUS
+
+endif # !CONFIG_UDEV
+
+EXTRA_DIST = xorg-server.conf x11-input.fdi 10-evdev.conf fdi2iclass.py
diff --git a/xorg-server/config/dbus.c b/xorg-server/config/dbus.c
index d6316623b..5d11feaa1 100644
--- a/xorg-server/config/dbus.c
+++ b/xorg-server/config/dbus.c
@@ -27,7 +27,6 @@
#include <dix-config.h>
#endif
-#define DBUS_API_SUBJECT_TO_CHANGE
#include <dbus/dbus.h>
#include <string.h>
@@ -184,10 +183,8 @@ unwind:
while (options) {
tmpo = options;
options = options->next;
- if (tmpo->key)
- free(tmpo->key);
- if (tmpo->value)
- free(tmpo->value);
+ free(tmpo->key);
+ free(tmpo->value);
free(tmpo);
}
diff --git a/xorg-server/config/fdi2iclass.py b/xorg-server/config/fdi2iclass.py
new file mode 100644
index 000000000..9dc06a76f
--- /dev/null
+++ b/xorg-server/config/fdi2iclass.py
@@ -0,0 +1,202 @@
+#!/usr/bin/python
+#
+# Convert xorg keys from hal FDIs files to xorg.conf InputClass sections.
+# Modified from Martin Pitt's original fdi2mpi.py script:
+# http://cgit.freedesktop.org/media-player-info/tree/tools/fdi2mpi.py
+#
+# (C) 2010 Dan Nicholson
+# (C) 2009 Canonical Ltd.
+# Author: Dan Nicholson <dbn.lists@gmail.com>
+# Author: Martin Pitt <martin.pitt@ubuntu.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# fur- nished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FIT- NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CON-
+# NECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import sys, xml.dom.minidom
+
+# dict converting <match> tags to Match* entries
+match_table = {
+ 'info.product': 'MatchProduct',
+ 'input.product': 'MatchProduct',
+ 'info.vendor': 'MatchVendor',
+ 'input.vendor': 'MatchVendor',
+ 'info.device': 'MatchDevicePath',
+ 'linux.device_file': 'MatchDevicePath',
+ '/org/freedesktop/Hal/devices/computer:system.kernel.name': 'MatchOS',
+ '@info.parent:pnp.id': 'MatchPnPID',
+}
+
+# dict converting info.capabilities list to Match* entries
+cap_match_table = {
+ 'input.keys': 'MatchIsKeyboard',
+ 'input.keyboard': 'MatchIsKeyboard',
+ 'input.keypad': 'MatchIsKeyboard',
+ 'input.mouse': 'MatchIsPointer',
+ 'input.joystick': 'MatchIsJoystick',
+ 'input.tablet': 'MatchIsTablet',
+ 'input.touchpad': 'MatchIsTouchpad',
+ 'input.touchscreen': 'MatchIsTouchscreen',
+}
+
+def device_glob(path):
+ '''Convert a contains device path to a glob entry'''
+ if path[0] != '/':
+ path = '*' + path
+ return path + '*'
+
+def parse_match(node):
+ '''Parse a <match> tag to a tuple with InputClass values'''
+ match = None
+ value = None
+ booltype = False
+
+ # see what type of key we have
+ if node.attributes.has_key('key'):
+ key = node.attributes['key'].nodeValue
+ if key in match_table:
+ match = match_table[key]
+ elif key == 'info.capabilities':
+ booltype = True
+
+ # bail out now if it's unrecognized
+ if not match and not booltype:
+ return (match, value)
+
+ if node.attributes.has_key('string'):
+ value = node.attributes['string'].nodeValue
+ elif node.attributes.has_key('contains'):
+ value = node.attributes['contains'].nodeValue
+ if match == 'MatchDevicePath':
+ value = device_glob(value)
+ elif booltype and value in cap_match_table:
+ match = cap_match_table[value]
+ value = 'yes'
+ elif node.attributes.has_key('string_outof'):
+ value = node.attributes['string_outof'].nodeValue.replace(';','|')
+ elif node.attributes.has_key('contains_outof'):
+ all_values = node.attributes['contains_outof'].nodeValue.split(';')
+ for v in all_values:
+ if match == 'MatchDevicePath':
+ v = device_glob(v)
+ elif match == 'MatchPnPID' and len(v) < 7:
+ v += '*'
+ if value:
+ value += '|' + v
+ else:
+ value = v
+
+ return (match, value)
+
+def parse_options(node):
+ '''Parse the x11_* options and return InputClass entries'''
+ driver = ''
+ ignore = False
+ options = []
+ for n in node.childNodes:
+ if n.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
+ continue
+
+ tag = n.tagName
+ key = n.attributes['key'].nodeValue
+ value = ''
+
+ if n.hasChildNodes():
+ content_node = n.childNodes[0]
+ assert content_node.nodeType == xml.dom.Node.TEXT_NODE
+ value = content_node.nodeValue
+
+ if tag == 'match':
+ continue
+ assert tag in ('addset', 'merge', 'append', 'remove')
+
+ if tag == 'remove' and key == 'input.x11_driver':
+ ignore = True
+ elif key == 'input.x11_driver':
+ driver = value
+ elif key.startswith('input.x11_options.'):
+ option = key.split('.', 2)[2]
+ options.append((option, value))
+
+ return (driver, ignore, options)
+
+def is_match_node(node):
+ '''Check if a node is a <match> element'''
+ return node.nodeType == xml.dom.minidom.Node.ELEMENT_NODE and \
+ node.tagName == 'match'
+
+def parse_all_matches(node):
+ '''Parse a x11 match tag and any parents that don't supply their
+ own options'''
+ matches = []
+
+ while True:
+ (key, value) = parse_match(node)
+ if key and value:
+ matches.append((key, value))
+
+ # walk up to a parent match node
+ node = node.parentNode
+ if node == None or not is_match_node(node):
+ break
+
+ # leave if there other options at this level
+ children = set([n.tagName for n in node.childNodes
+ if n.nodeType == xml.dom.minidom.Node.ELEMENT_NODE])
+ if children & set(['addset', 'merge', 'append']):
+ break
+
+ return matches
+
+# stupid counter to give "unique" rule names
+num_sections = 1
+def print_section(matches, driver, ignore, options):
+ '''Print a valid InputClass section to stdout'''
+ global num_sections
+ print 'Section "InputClass"'
+ print '\tIdentifier "Converted Class %d"' % num_sections
+ num_sections += 1
+ for m, v in matches:
+ print '\t%s "%s"' % (m, v)
+ if driver:
+ print '\tDriver "%s"' % driver
+ if ignore:
+ print '\tOption "Ignore" "yes"'
+ for o, v in options:
+ print '\tOption "%s" "%s"' % (o, v)
+ print 'EndSection'
+
+def parse_fdi(fdi):
+ '''Parse x11 matches from fdi'''
+ # find all <match> leaf nodes
+ num = 0
+ for match_node in fdi.getElementsByTagName('match'):
+ children = set([n.tagName for n in match_node.childNodes
+ if n.nodeType == xml.dom.minidom.Node.ELEMENT_NODE])
+
+ # see if there are any options at this level
+ (driver, ignore, options) = parse_options(match_node)
+ if not driver and not ignore and not options:
+ continue
+
+ matches = parse_all_matches(match_node)
+ if num > 0:
+ print
+ print_section(matches, driver, ignore, options)
+ num += 1
+
+for f in sys.argv[1:]:
+ parse_fdi(xml.dom.minidom.parse(f))
diff --git a/xorg-server/config/hal.c b/xorg-server/config/hal.c
index 738cb3185..1245bb192 100644
--- a/xorg-server/config/hal.c
+++ b/xorg-server/config/hal.c
@@ -129,6 +129,7 @@ static void
device_added(LibHalContext *hal_ctx, const char *udi)
{
char *path = NULL, *driver = NULL, *name = NULL, *config_info = NULL;
+ char *hal_tags, *parent;
InputOption *options = NULL, *tmpo = NULL;
InputAttributes attrs = {0};
DeviceIntPtr dev = NULL;
@@ -164,7 +165,9 @@ device_added(LibHalContext *hal_ctx, const char *udi)
attrs.product = xstrdup(name);
attrs.vendor = get_prop_string(hal_ctx, udi, "info.vendor");
- attrs.tags = xstrtokenize(get_prop_string(hal_ctx, udi, "input.tags"), ",");
+ hal_tags = get_prop_string(hal_ctx, udi, "input.tags");
+ attrs.tags = xstrtokenize(hal_tags, ",");
+ free(hal_tags);
if (libhal_device_query_capability(hal_ctx, udi, "input.keys", NULL))
attrs.flags |= ATTR_KEYBOARD;
@@ -179,6 +182,29 @@ device_added(LibHalContext *hal_ctx, const char *udi)
if (libhal_device_query_capability(hal_ctx, udi, "input.touchscreen", NULL))
attrs.flags |= ATTR_TOUCHSCREEN;
+ parent = get_prop_string(hal_ctx, udi, "info.parent");
+ if (parent) {
+ int usb_vendor, usb_product;
+
+ attrs.pnp_id = get_prop_string(hal_ctx, parent, "pnp.id");
+
+ /* construct USB ID in lowercase - "0000:ffff" */
+ usb_vendor = libhal_device_get_property_int(hal_ctx, parent,
+ "usb.vendor_id", NULL);
+ LogMessageVerb(X_INFO, 10,
+ "config/hal: getting usb.vendor_id on %s "
+ "returned %04x\n", parent, usb_vendor);
+ usb_product = libhal_device_get_property_int(hal_ctx, parent,
+ "usb.product_id", NULL);
+ LogMessageVerb(X_INFO, 10,
+ "config/hal: getting usb.product_id on %s "
+ "returned %04x\n", parent, usb_product);
+ if (usb_vendor && usb_product)
+ attrs.usb_id = Xprintf("%04x:%04x", usb_vendor, usb_product);
+
+ free(parent);
+ }
+
options = calloc(sizeof(*options), 1);
if (!options){
LogMessage(X_ERROR, "config/hal: couldn't allocate space for input options!\n");
@@ -251,28 +277,23 @@ device_added(LibHalContext *hal_ctx, const char *udi)
{
if (!strcasecmp(&tmp[3], "layout"))
{
- if (xkb_opts.layout)
- free(xkb_opts.layout);
+ free(xkb_opts.layout);
xkb_opts.layout = strdup(tmp_val);
} else if (!strcasecmp(&tmp[3], "model"))
{
- if (xkb_opts.model)
- free(xkb_opts.model);
+ free(xkb_opts.model);
xkb_opts.model = strdup(tmp_val);
} else if (!strcasecmp(&tmp[3], "rules"))
{
- if (xkb_opts.rules)
- free(xkb_opts.rules);
+ free(xkb_opts.rules);
xkb_opts.rules = strdup(tmp_val);
} else if (!strcasecmp(&tmp[3], "variant"))
{
- if (xkb_opts.variant)
- free(xkb_opts.variant);
+ free(xkb_opts.variant);
xkb_opts.variant = strdup(tmp_val);
} else if (!strcasecmp(&tmp[3], "options"))
{
- if (xkb_opts.options)
- free(xkb_opts.options);
+ free(xkb_opts.options);
xkb_opts.options = strdup(tmp_val);
}
} else
@@ -289,8 +310,7 @@ device_added(LibHalContext *hal_ctx, const char *udi)
(!strcasecmp(&tmp[3], "options")) &&
(tmp_val = get_prop_string_array(hal_ctx, udi, psi_key)))
{
- if (xkb_opts.options)
- free(xkb_opts.options);
+ free(xkb_opts.options);
xkb_opts.options = strdup(tmp_val);
}
}
@@ -366,22 +386,17 @@ device_added(LibHalContext *hal_ctx, const char *udi)
}
for (; dev; dev = dev->next){
- if (dev->config_info)
- free(dev->config_info);
+ free(dev->config_info);
dev->config_info = xstrdup(config_info);
}
unwind:
if (set)
libhal_free_property_set(set);
- if (path)
- free(path);
- if (driver)
- free(driver);
- if (name)
- free(name);
- if (config_info)
- free(config_info);
+ free(path);
+ free(driver);
+ free(name);
+ free(config_info);
while (!dev && (tmpo = options)) {
options = tmpo->next;
free(tmpo->key);
@@ -392,6 +407,8 @@ unwind:
free(attrs.product);
free(attrs.vendor);
free(attrs.device);
+ free(attrs.pnp_id);
+ free(attrs.usb_id);
if (attrs.tags) {
char **tag = attrs.tags;
while (*tag) {
@@ -401,16 +418,11 @@ unwind:
free(attrs.tags);
}
- if (xkb_opts.layout)
- free(xkb_opts.layout);
- if (xkb_opts.rules)
- free(xkb_opts.rules);
- if (xkb_opts.model)
- free(xkb_opts.model);
- if (xkb_opts.variant)
- free(xkb_opts.variant);
- if (xkb_opts.options)
- free(xkb_opts.options);
+ free(xkb_opts.layout);
+ free(xkb_opts.rules);
+ free(xkb_opts.model);
+ free(xkb_opts.variant);
+ free(xkb_opts.options);
dbus_error_free(&error);
@@ -635,7 +647,7 @@ config_hal_init(void)
}
/* verbose message */
- LogMessageVerb(X_INFO,7,"config/hal: initialized");
+ LogMessageVerb(X_INFO,7,"config/hal: initialized\n");
return 1;
}
diff --git a/xorg-server/config/udev.c b/xorg-server/config/udev.c
index 304ee2c93..bf463168e 100644
--- a/xorg-server/config/udev.c
+++ b/xorg-server/config/udev.c
@@ -28,6 +28,7 @@
#endif
#include <libudev.h>
+#include <ctype.h>
#include "input.h"
#include "inputstr.h"
@@ -37,6 +38,17 @@
#define UDEV_XKB_PROP_KEY "xkb"
+#define LOG_PROPERTY(path, prop, val) \
+ LogMessageVerb(X_INFO, 10, \
+ "config/udev: getting property %s on %s " \
+ "returned \"%s\"\n", \
+ (prop), (path), (val) ? (val) : "(null)")
+#define LOG_SYSATTR(path, attr, val) \
+ LogMessageVerb(X_INFO, 10, \
+ "config/udev: getting attribute %s on %s " \
+ "returned \"%s\"\n", \
+ (attr), (path), (val) ? (val) : "(null)")
+
static struct udev_monitor *udev_monitor;
static void
@@ -45,6 +57,8 @@ device_added(struct udev_device *udev_device)
const char *path, *name = NULL;
char *config_info = NULL;
const char *syspath;
+ const char *tags_prop;
+ const char *usb_vendor = NULL, *usb_model = NULL;
const char *key, *value, *tmp;
InputOption *options = NULL, *tmpo;
InputAttributes attrs = {};
@@ -60,8 +74,13 @@ device_added(struct udev_device *udev_device)
if (!path || !syspath)
return;
- if (!udev_device_get_property_value(udev_device, "ID_INPUT"))
+ if (!udev_device_get_property_value(udev_device, "ID_INPUT")) {
+ LogMessageVerb(X_INFO, 10,
+ "config/udev: ignoring device %s without "
+ "property ID_INPUT set\n",
+ path);
return;
+ }
options = calloc(sizeof(*options), 1);
if (!options)
@@ -74,9 +93,17 @@ device_added(struct udev_device *udev_device)
parent = udev_device_get_parent(udev_device);
if (parent) {
+ const char *ppath = udev_device_get_devnode(parent);
+
name = udev_device_get_sysattr_value(parent, "name");
- if (!name)
+ LOG_SYSATTR(ppath, "name", name);
+ if (!name) {
name = udev_device_get_property_value(parent, "NAME");
+ LOG_PROPERTY(ppath, "NAME", name);
+ }
+
+ attrs.pnp_id = udev_device_get_sysattr_value(parent, "id");
+ LOG_SYSATTR(ppath, "id", attrs.pnp_id);
}
if (!name)
name = "(unnamed)";
@@ -87,7 +114,10 @@ device_added(struct udev_device *udev_device)
add_option(&options, "path", path);
add_option(&options, "device", path);
attrs.device = path;
- attrs.tags = xstrtokenize(udev_device_get_property_value(udev_device, "ID_INPUT.tags"), ",");
+
+ tags_prop = udev_device_get_property_value(udev_device, "ID_INPUT.tags");
+ LOG_PROPERTY(path, "ID_INPUT.tags", tags_prop);
+ attrs.tags = xstrtokenize(tags_prop, ",");
config_info = Xprintf("udev:%s", syspath);
if (!config_info)
@@ -107,6 +137,7 @@ device_added(struct udev_device *udev_device)
value = udev_list_entry_get_value(entry);
if (!strncasecmp(key, UDEV_XKB_PROP_KEY,
sizeof(UDEV_XKB_PROP_KEY) - 1)) {
+ LOG_PROPERTY(path, key, value);
tmp = key + sizeof(UDEV_XKB_PROP_KEY) - 1;
if (!strcasecmp(tmp, "rules"))
add_option(&options, "xkb_rules", value);
@@ -119,21 +150,45 @@ device_added(struct udev_device *udev_device)
else if (!strcasecmp(tmp, "options"))
add_option(&options, "xkb_options", value);
} else if (!strcmp(key, "ID_VENDOR")) {
+ LOG_PROPERTY(path, key, value);
attrs.vendor = value;
+ } else if (!strcmp(key, "ID_VENDOR_ID")) {
+ LOG_PROPERTY(path, key, value);
+ usb_vendor = value;
+ } else if (!strcmp(key, "ID_VENDOR_MODEL")) {
+ LOG_PROPERTY(path, key, value);
+ usb_model = value;
} else if (!strcmp(key, "ID_INPUT_KEY")) {
+ LOG_PROPERTY(path, key, value);
attrs.flags |= ATTR_KEYBOARD;
} else if (!strcmp(key, "ID_INPUT_MOUSE")) {
+ LOG_PROPERTY(path, key, value);
attrs.flags |= ATTR_POINTER;
} else if (!strcmp(key, "ID_INPUT_JOYSTICK")) {
+ LOG_PROPERTY(path, key, value);
attrs.flags |= ATTR_JOYSTICK;
} else if (!strcmp(key, "ID_INPUT_TABLET")) {
+ LOG_PROPERTY(path, key, value);
attrs.flags |= ATTR_TABLET;
} else if (!strcmp(key, "ID_INPUT_TOUCHPAD")) {
+ LOG_PROPERTY(path, key, value);
attrs.flags |= ATTR_TOUCHPAD;
} else if (!strcmp(key, "ID_INPUT_TOUCHSCREEN")) {
+ LOG_PROPERTY(path, key, value);
attrs.flags |= ATTR_TOUCHSCREEN;
}
}
+
+ /* construct USB ID in lowercase hex - "0000:ffff" */
+ if (usb_vendor && usb_model) {
+ attrs.usb_id = Xprintf("%s:%s", usb_vendor, usb_model);
+ if (attrs.usb_id) {
+ char *cur;
+ for (cur = attrs.usb_id; *cur; cur++)
+ *cur = tolower(*cur);
+ }
+ }
+
LogMessage(X_INFO, "config/udev: Adding input device %s (%s)\n",
name, path);
rc = NewInputDeviceRequest(options, &attrs, &dev);
@@ -154,6 +209,7 @@ device_added(struct udev_device *udev_device)
free(tmpo);
}
+ free(attrs.usb_id);
if (attrs.tags) {
char **tag = attrs.tags;
while (*tag) {