1 /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  *  Copyright (C) 2005 Takuro Ashie <ashie@homa.ne.jp>
4  *  Copyright (C) 2006 Juernjakob Harder <juernjakob.harder@gmail.com>
5  *
6  *  This library is free software; you can redistribute it and/or
7  *  modify it under the terms of the GNU Lesser General Public
8  *  License as published by the Free Software Foundation; either
9  *  version 2 of the License, or (at your option) any later version.
10  *
11  *  This library is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU Lesser General Public License for more details.
15  *
16  *  You should have received a copy of the GNU Lesser General Public
17  *  License along with this program; if not, write to the
18  *  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
19  *  Boston, MA  02111-1307  USA
20  *
21  *  $Id: tomoe-char.c 1488 2007-06-16 01:16:23Z ikezoe $
22  */
23 
24 #ifdef HAVE_CONFIG_H
25 #include "config.h"
26 #endif /* HAVE_CONFIG_H */
27 
28 #include <stdlib.h>
29 #include <string.h>
30 #include <glib.h>
31 #include <glib/gi18n-lib.h>
32 
33 #include "tomoe-char.h"
34 #include "tomoe-dict.h"
35 #include "tomoe-xml-parser.h"
36 #include "glib-utils.h"
37 
38 #define TOMOE_CHAR_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), TOMOE_TYPE_CHAR, TomoeCharPrivate))
39 
40 typedef struct _TomoeCharPrivate	TomoeCharPrivate;
41 struct _TomoeCharPrivate
42 {
43     gchar                *utf8;
44     gint                  n_strokes;
45     GList                *readings;
46     GList                *radicals;
47     TomoeWriting         *writing;
48     gchar                *variant;
49     GHashTable           *meta_data;
50 };
51 
52 enum
53 {
54     PROP_0,
55     PROP_UTF8,
56     PROP_N_STROKES,
57     PROP_WRITING,
58     PROP_VARIANT
59 };
60 
61 G_DEFINE_TYPE (TomoeChar, tomoe_char, G_TYPE_OBJECT)
62 
63 static void dispose        (GObject         *object);
64 static void set_property   (GObject         *object,
65                             guint            prop_id,
66                             const GValue    *value,
67                             GParamSpec      *pspec);
68 static void get_property   (GObject         *object,
69                             guint            prop_id,
70                             GValue          *value,
71                             GParamSpec      *pspec);
72 
73 static void
tomoe_char_class_init(TomoeCharClass * klass)74 tomoe_char_class_init (TomoeCharClass *klass)
75 {
76     GObjectClass *gobject_class;
77     GParamSpec *spec;
78 
79     gobject_class = G_OBJECT_CLASS (klass);
80 
81     gobject_class->dispose      = dispose;
82     gobject_class->set_property = set_property;
83     gobject_class->get_property = get_property;
84 
85     spec = g_param_spec_string ("utf8",
86                                 N_("UTF8"),
87                                 N_("UTF8 encoding of the character."),
88                                 NULL,
89                                 G_PARAM_READABLE | G_PARAM_WRITABLE);
90     g_object_class_install_property (gobject_class, PROP_UTF8, spec);
91 
92     spec = g_param_spec_int ("n_strokes",
93                              N_("Number of strokes"),
94                              N_("Number of strokes of the character."),
95                              -1, G_MAXINT32, -1,
96                              G_PARAM_READABLE | G_PARAM_WRITABLE);
97     g_object_class_install_property (gobject_class, PROP_N_STROKES, spec);
98 
99     spec = g_param_spec_object ("writing",
100                                 N_("Writing"),
101                                 N_("Writing of the character."),
102                                 TOMOE_TYPE_WRITING,
103                                 G_PARAM_READABLE | G_PARAM_WRITABLE);
104     g_object_class_install_property (gobject_class, PROP_WRITING, spec);
105 
106     spec = g_param_spec_string ("variant",
107                                 N_("Variant"),
108                                 N_("Variant of the character."),
109                                 NULL,
110                                 G_PARAM_READABLE | G_PARAM_WRITABLE);
111     g_object_class_install_property (gobject_class, PROP_VARIANT, spec);
112 
113     g_type_class_add_private (gobject_class, sizeof (TomoeCharPrivate));
114 }
115 
116 static void
tomoe_char_init(TomoeChar * chr)117 tomoe_char_init (TomoeChar *chr)
118 {
119     TomoeCharPrivate *priv = TOMOE_CHAR_GET_PRIVATE (chr);
120     priv->utf8       = NULL;
121     priv->n_strokes  = -1;
122     priv->readings   = NULL;
123     priv->radicals   = NULL;
124     priv->writing    = NULL;
125     priv->variant    = NULL;
126     priv->meta_data  = g_hash_table_new_full(g_str_hash, g_str_equal,
127                                              g_free, g_free);
128 }
129 
130 /**
131  * tomoe_char_new:
132  *
133  * Create a new #TomoeChar.
134  *
135  * Return value: a new #TomoeChar
136  */
137 TomoeChar*
tomoe_char_new(void)138 tomoe_char_new (void)
139 {
140     return g_object_new(TOMOE_TYPE_CHAR, NULL);
141 }
142 
143 TomoeChar*
tomoe_char_new_from_xml_data(const gchar * data,gssize len)144 tomoe_char_new_from_xml_data (const gchar *data, gssize len)
145 {
146     return _tomoe_xml_parser_parse_char_data (data, len);
147 }
148 
149 static void
copy_meta_data(gpointer key,gpointer value,gpointer user_data)150 copy_meta_data (gpointer key, gpointer value, gpointer user_data)
151 {
152     TomoeChar *chr = user_data;
153     gchar *meta_key = key;
154     gchar *meta_value = value;
155 
156     tomoe_char_register_meta_data (chr, meta_key, meta_value);
157 }
158 
159 TomoeChar*
tomoe_char_dup(TomoeChar * chr)160 tomoe_char_dup (TomoeChar *chr)
161 {
162     TomoeChar *new_chr;
163     TomoeCharPrivate *priv;
164 
165     new_chr = tomoe_char_new ();
166     priv = TOMOE_CHAR_GET_PRIVATE (chr);
167 
168     tomoe_char_set_utf8 (new_chr, priv->utf8);
169     tomoe_char_set_n_strokes (new_chr, priv->n_strokes);
170 
171     if (priv->writing) {
172         TomoeWriting *new_writing = tomoe_writing_dup (priv->writing);
173         tomoe_char_set_writing (new_chr, new_writing);
174         g_object_unref (new_writing);
175     }
176 
177     if (priv->variant)
178         tomoe_char_set_variant (new_chr, priv->variant);
179 
180     if (priv->readings) {
181         GList *node;
182         for (node = g_list_last(priv->readings); node; node = g_list_previous (node)) {
183             TomoeReading *new_reading = tomoe_reading_dup (TOMOE_READING(node->data));
184             tomoe_char_add_reading (new_chr, new_reading);
185             g_object_unref (new_reading);
186         }
187     }
188 
189     if (priv->radicals) {
190         GList *node;
191         for (node = g_list_last(priv->radicals); node; node = g_list_previous (node)) {
192             tomoe_char_add_radical (new_chr, node->data);
193         }
194     }
195 
196     if (priv->meta_data) {
197         tomoe_char_meta_data_foreach (chr, (GHFunc) copy_meta_data, new_chr);
198     }
199 
200     return new_chr;
201 }
202 
203 static void
dispose(GObject * object)204 dispose (GObject *object)
205 {
206     TomoeCharPrivate *priv = TOMOE_CHAR_GET_PRIVATE (object);
207 
208     if (priv->utf8)
209         g_free (priv->utf8);
210     if (priv->readings) {
211         g_list_foreach (priv->readings, (GFunc)g_object_unref, NULL);
212         g_list_free (priv->readings);
213     }
214     if (priv->radicals) {
215         g_list_foreach (priv->radicals, (GFunc)g_free, NULL);
216         g_list_free (priv->radicals);
217     }
218     if (priv->writing)
219         g_object_unref (G_OBJECT (priv->writing));
220     if (priv->variant)
221         g_free (priv->variant);
222     if (priv->meta_data)
223         g_hash_table_destroy (priv->meta_data);
224 
225     priv->utf8      = NULL;
226     priv->n_strokes = -1;
227     priv->readings  = NULL;
228     priv->radicals  = NULL;
229     priv->writing   = NULL;
230     priv->variant   = NULL;
231     priv->meta_data = NULL;
232 
233     G_OBJECT_CLASS (tomoe_char_parent_class)->dispose (object);
234 }
235 
236 static void
set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)237 set_property (GObject      *object,
238               guint         prop_id,
239               const GValue *value,
240               GParamSpec   *pspec)
241 {
242     TomoeChar *chr;
243 
244     chr = TOMOE_CHAR(object);
245     switch (prop_id) {
246       case PROP_UTF8:
247         tomoe_char_set_utf8 (chr, g_value_get_string (value));
248         break;
249       case PROP_N_STROKES:
250         tomoe_char_set_n_strokes (chr, g_value_get_int (value));
251         break;
252       case PROP_WRITING:
253         tomoe_char_set_writing (chr, g_value_get_object (value));
254         break;
255       case PROP_VARIANT:
256         tomoe_char_set_variant (chr, g_value_get_string (value));
257         break;
258       default:
259         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
260         break;
261     }
262 }
263 
264 static void
get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)265 get_property (GObject    *object,
266               guint       prop_id,
267               GValue     *value,
268               GParamSpec *pspec)
269 {
270     TomoeChar *chr;
271     TomoeCharPrivate *priv;
272 
273     chr = TOMOE_CHAR (object);
274     priv = TOMOE_CHAR_GET_PRIVATE (chr);
275 
276     switch (prop_id) {
277       case PROP_UTF8:
278         g_value_set_string (value, priv->utf8);
279         break;
280       case PROP_N_STROKES:
281         g_value_set_int (value, priv->n_strokes);
282         break;
283       case PROP_WRITING:
284         g_value_set_object (value, priv->writing);
285         break;
286       case PROP_VARIANT:
287         g_value_set_string (value, priv->variant);
288         break;
289       default:
290         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
291         break;
292     }
293 }
294 
295 const gchar *
tomoe_char_get_utf8(TomoeChar * chr)296 tomoe_char_get_utf8 (TomoeChar *chr)
297 {
298     TomoeCharPrivate *priv;
299 
300     g_return_val_if_fail (TOMOE_IS_CHAR (chr), NULL);
301 
302     priv = TOMOE_CHAR_GET_PRIVATE (chr);
303     return priv->utf8;
304 }
305 
306 void
tomoe_char_set_utf8(TomoeChar * chr,const gchar * utf8)307 tomoe_char_set_utf8 (TomoeChar *chr, const gchar *utf8)
308 {
309     TomoeCharPrivate *priv;
310 
311     g_return_if_fail (TOMOE_IS_CHAR (chr));
312 
313     priv = TOMOE_CHAR_GET_PRIVATE (chr);
314     if (priv->utf8)
315         g_free (priv->utf8);
316     priv->utf8 = utf8 ? g_strdup (utf8) : NULL;
317 }
318 
319 gint
tomoe_char_get_n_strokes(TomoeChar * chr)320 tomoe_char_get_n_strokes (TomoeChar *chr)
321 {
322     TomoeCharPrivate *priv;
323 
324     g_return_val_if_fail (TOMOE_IS_CHAR (chr), 0);
325 
326     priv = TOMOE_CHAR_GET_PRIVATE (chr);
327     return priv->n_strokes;
328 }
329 
330 void
tomoe_char_set_n_strokes(TomoeChar * chr,gint n_strokes)331 tomoe_char_set_n_strokes (TomoeChar *chr, gint n_strokes)
332 {
333     TomoeCharPrivate *priv;
334 
335     g_return_if_fail (TOMOE_IS_CHAR (chr));
336 
337     priv = TOMOE_CHAR_GET_PRIVATE (chr);
338     priv->n_strokes = n_strokes;
339 }
340 
341 const GList *
tomoe_char_get_readings(TomoeChar * chr)342 tomoe_char_get_readings (TomoeChar* chr)
343 {
344     TomoeCharPrivate *priv;
345 
346     g_return_val_if_fail (TOMOE_IS_CHAR (chr), NULL);
347 
348     priv = TOMOE_CHAR_GET_PRIVATE (chr);
349     return priv->readings;
350 }
351 
352 void
tomoe_char_add_reading(TomoeChar * chr,TomoeReading * reading)353 tomoe_char_add_reading (TomoeChar* chr, TomoeReading *reading)
354 {
355     TomoeCharPrivate *priv;
356 
357     g_return_if_fail (TOMOE_IS_CHAR (chr));
358 
359     priv = TOMOE_CHAR_GET_PRIVATE (chr);
360 
361     priv->readings = g_list_prepend (priv->readings, g_object_ref (reading));
362 }
363 
364 const GList *
tomoe_char_get_radicals(TomoeChar * chr)365 tomoe_char_get_radicals (TomoeChar* chr)
366 {
367     TomoeCharPrivate *priv;
368 
369     g_return_val_if_fail (TOMOE_IS_CHAR (chr), NULL);
370 
371     priv = TOMOE_CHAR_GET_PRIVATE (chr);
372     return priv->radicals;
373 }
374 
375 void
tomoe_char_add_radical(TomoeChar * chr,const gchar * radical)376 tomoe_char_add_radical (TomoeChar* chr, const gchar *radical)
377 {
378     TomoeCharPrivate *priv;
379 
380     g_return_if_fail (TOMOE_IS_CHAR (chr));
381     g_return_if_fail (radical && radical[0] != '\0');
382 
383     priv = TOMOE_CHAR_GET_PRIVATE (chr);
384 
385     priv->radicals = g_list_prepend (priv->radicals, g_strdup (radical));
386 }
387 
388 TomoeWriting *
tomoe_char_get_writing(TomoeChar * chr)389 tomoe_char_get_writing (TomoeChar *chr)
390 {
391     TomoeCharPrivate *priv;
392 
393     g_return_val_if_fail (TOMOE_IS_CHAR (chr), NULL);
394 
395     priv = TOMOE_CHAR_GET_PRIVATE (chr);
396 
397     return priv->writing;
398 }
399 
400 void
tomoe_char_set_writing(TomoeChar * chr,TomoeWriting * writing)401 tomoe_char_set_writing (TomoeChar *chr, TomoeWriting *writing)
402 {
403     TomoeCharPrivate *priv;
404 
405     g_return_if_fail (TOMOE_IS_CHAR (chr));
406 
407     priv = TOMOE_CHAR_GET_PRIVATE (chr);
408 
409     if (priv->writing)
410         g_object_unref (G_OBJECT (priv->writing));
411     priv->writing = g_object_ref (writing);
412 }
413 
414 const gchar *
tomoe_char_get_variant(TomoeChar * chr)415 tomoe_char_get_variant (TomoeChar *chr)
416 {
417     TomoeCharPrivate *priv;
418 
419     g_return_val_if_fail (TOMOE_IS_CHAR (chr), NULL);
420 
421     priv = TOMOE_CHAR_GET_PRIVATE (chr);
422 
423     return priv->variant;
424 }
425 
426 void
tomoe_char_set_variant(TomoeChar * chr,const gchar * variant)427 tomoe_char_set_variant (TomoeChar *chr, const gchar *variant)
428 {
429     TomoeCharPrivate *priv;
430 
431     g_return_if_fail (TOMOE_IS_CHAR (chr));
432 
433     priv = TOMOE_CHAR_GET_PRIVATE (chr);
434 
435     if (priv->variant)
436         g_free (priv->variant);
437     priv->variant = variant ? g_strdup (variant) : NULL;
438 }
439 
440 void
tomoe_char_register_meta_data(TomoeChar * chr,const gchar * key,const gchar * value)441 tomoe_char_register_meta_data (TomoeChar *chr, const gchar *key,
442                                const gchar *value)
443 {
444     TomoeCharPrivate *priv;
445     g_return_if_fail (chr);
446     g_return_if_fail (key);
447     g_return_if_fail (value);
448 
449     priv = TOMOE_CHAR_GET_PRIVATE (chr);
450     g_hash_table_insert (priv->meta_data, g_strdup (key), g_strdup (value));
451 }
452 
453 const gchar*
tomoe_char_get_meta_data(TomoeChar * chr,const gchar * key)454 tomoe_char_get_meta_data (TomoeChar* chr, const gchar *key)
455 {
456     TomoeCharPrivate *priv;
457     g_return_val_if_fail (chr, NULL);
458     g_return_val_if_fail (key, NULL);
459 
460     priv = TOMOE_CHAR_GET_PRIVATE (chr);
461     return g_hash_table_lookup (priv->meta_data, key);
462 }
463 
464 gboolean
tomoe_char_has_meta_data(TomoeChar * chr)465 tomoe_char_has_meta_data (TomoeChar *chr)
466 {
467     TomoeCharPrivate *priv;
468     g_return_val_if_fail (chr, FALSE);
469 
470     priv = TOMOE_CHAR_GET_PRIVATE (chr);
471     return g_hash_table_size (priv->meta_data) > 0;
472 }
473 
474 void
tomoe_char_meta_data_foreach(TomoeChar * chr,GHFunc func,gpointer user_data)475 tomoe_char_meta_data_foreach (TomoeChar* chr, GHFunc func, gpointer user_data)
476 {
477     TomoeCharPrivate *priv;
478     g_return_if_fail (chr);
479 
480     priv = TOMOE_CHAR_GET_PRIVATE (chr);
481     g_hash_table_foreach (priv->meta_data, func, user_data);
482 }
483 
484 /**
485  * tomoe_char_compare:
486  * @a: a TomoeChar object.
487  * @b: a TomoeChar object to compare with.
488  *
489  * Compare to TomoeChar objects with its own utf8 character.
490  *
491  * Return value: -1 a < b, 0 a= b, 1 a > b
492  */
493 gint
tomoe_char_compare(const TomoeChar * a,const TomoeChar * b)494 tomoe_char_compare (const TomoeChar *a, const TomoeChar *b)
495 {
496     TomoeCharPrivate *priv_a, *priv_b;
497 
498     if (!a || !b) return 0;
499 
500     priv_a = TOMOE_CHAR_GET_PRIVATE (a);
501     priv_b = TOMOE_CHAR_GET_PRIVATE (b);
502     if (!priv_a || !priv_b) return 0;
503 
504     if (!priv_a->utf8 || !priv_b->utf8) return 0;
505     return strcmp (priv_a->utf8, priv_b->utf8);
506 }
507 
508 
509 static void
tomoe_char_to_xml_utf8(TomoeChar * chr,TomoeCharPrivate * priv,GString * output)510 tomoe_char_to_xml_utf8 (TomoeChar *chr, TomoeCharPrivate *priv,
511                         GString *output)
512 {
513     gchar *xml;
514 
515     if (!priv->utf8) return;
516 
517     xml = g_markup_printf_escaped ("    <utf8>%s</utf8>\n", priv->utf8);
518     g_string_append (output, xml);
519     g_free (xml);
520 }
521 
522 static void
tomoe_char_to_xml_variant(TomoeChar * chr,TomoeCharPrivate * priv,GString * output)523 tomoe_char_to_xml_variant (TomoeChar *chr, TomoeCharPrivate *priv,
524                            GString *output)
525 {
526     gchar *xml;
527 
528     if (!priv->variant) return;
529 
530     xml = g_markup_printf_escaped ("    <variant>%s</variant>\n",
531                                    priv->variant);
532     g_string_append (output, xml);
533     g_free (xml);
534 }
535 
536 static void
tomoe_char_to_xml_readings(TomoeChar * chr,TomoeCharPrivate * priv,GString * output)537 tomoe_char_to_xml_readings (TomoeChar *chr, TomoeCharPrivate *priv,
538                             GString *output)
539 {
540     GList *node;
541 
542     if (!priv->readings) return;
543 
544     g_string_append (output, "    <readings>\n");
545     for (node = g_list_last (priv->readings); node; node = g_list_previous (node)) {
546         TomoeReading *reading = node->data;
547         gchar *xml;
548 
549         xml = tomoe_reading_to_xml (reading);
550         if (xml) {
551             g_string_append (output, xml);
552             g_free (xml);
553         }
554     }
555     g_string_append (output, "    </readings>\n");
556 }
557 
558 static void
tomoe_char_to_xml_radicals(TomoeChar * chr,TomoeCharPrivate * priv,GString * output)559 tomoe_char_to_xml_radicals (TomoeChar *chr, TomoeCharPrivate *priv,
560                             GString *output)
561 {
562     GList *node;
563 
564     if (!priv->radicals) return;
565 
566     g_string_append (output, "    <radicals>\n");
567     for (node = priv->radicals; node; node = g_list_next (node)) {
568         const gchar *radical = node->data;
569         gchar *xml;
570 
571         xml = g_markup_printf_escaped (radical, -1);
572         g_string_append_printf (output, "      <radical>%s</radical>\n", xml);
573         g_free (xml);
574     }
575     g_string_append (output, "    </radicals>\n");
576 }
577 
578 static void
tomoe_char_to_xml_writing(TomoeChar * chr,TomoeCharPrivate * priv,GString * output)579 tomoe_char_to_xml_writing (TomoeChar *chr, TomoeCharPrivate *priv,
580                            GString *output)
581 {
582     gchar *xml;
583 
584     if (!priv->writing) return;
585 
586     xml = tomoe_writing_to_xml (priv->writing);
587 
588     if (xml && xml[0] != '\0') {
589         g_string_append (output, xml);
590         g_free (xml);
591     }
592 }
593 
594 static void
tomoe_char_to_xml_n_strokes(TomoeChar * chr,TomoeCharPrivate * priv,GString * output)595 tomoe_char_to_xml_n_strokes (TomoeChar *chr, TomoeCharPrivate *priv,
596                              GString *output)
597 {
598     if (priv->n_strokes < 0) return;
599 
600     g_string_append_printf (
601         output,
602         "    <number-of-strokes>%d</number-of-strokes>\n",
603         priv->n_strokes);
604 }
605 
606 
607 static void
tomoe_char_to_xml_meta_datum(gpointer key,gpointer value,gpointer user_data)608 tomoe_char_to_xml_meta_datum (gpointer key, gpointer value, gpointer user_data)
609 {
610     GString *output = user_data;
611     gchar *meta_key = key;
612     gchar *meta_value = value;
613     gchar *result;
614 
615     result = g_markup_printf_escaped ("      <%s>%s</%s>\n",
616                                       meta_key, meta_value, meta_key);
617     g_string_append (output, result);
618     g_free (result);
619 }
620 
621 static void
tomoe_char_to_xml_meta(TomoeChar * chr,TomoeCharPrivate * priv,GString * output)622 tomoe_char_to_xml_meta (TomoeChar *chr, TomoeCharPrivate *priv, GString *output)
623 {
624     if (!tomoe_char_has_meta_data (chr)) return;
625 
626     g_string_append (output, "    <meta>\n");
627     tomoe_char_meta_data_foreach (chr, tomoe_char_to_xml_meta_datum, output);
628     g_string_append (output, "    </meta>\n");
629 }
630 
631 gchar *
tomoe_char_to_xml(TomoeChar * chr)632 tomoe_char_to_xml (TomoeChar* chr)
633 {
634     TomoeCharPrivate *priv;
635     GString *output;
636 
637     g_return_val_if_fail (TOMOE_IS_CHAR (chr), NULL);
638 
639     priv = TOMOE_CHAR_GET_PRIVATE (chr);
640     output = g_string_new ("");
641 
642     tomoe_char_to_xml_utf8 (chr, priv, output);
643     tomoe_char_to_xml_variant (chr, priv, output);
644     tomoe_char_to_xml_readings (chr, priv, output);
645     tomoe_char_to_xml_radicals (chr, priv, output);
646     tomoe_char_to_xml_n_strokes (chr, priv, output);
647     tomoe_char_to_xml_writing (chr, priv, output);
648     tomoe_char_to_xml_meta (chr, priv, output);
649 
650     if (output->len > 0) {
651         g_string_prepend (output, "  <character>\n");
652         g_string_append (output, "  </character>\n");
653     }
654 
655     return g_string_free (output, FALSE);
656 }
657 
658 
659 /*
660 vi:ts=4:nowrap:ai:expandtab
661 */
662