From eaa100485541ae65959f78f6ae27e8c5cc1c644d Mon Sep 17 00:00:00 2001 From: Steven Schubiger Date: Tue, 18 Dec 2012 23:07:43 +0100 Subject: Initial commit. --- Makefile | 20 ++ colorize.c | 730 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test.pl | 91 ++++++++ 3 files changed, 841 insertions(+) create mode 100644 Makefile create mode 100644 colorize.c create mode 100755 test.pl diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b332cda --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: clean + +.SUFFIXES: +.SUFFIXES: .c .o + +SHELL=/bin/sh +CC=gcc +CFLAGS=-Wall -Wextra -Wformat -Wswitch-default -Wuninitialized -Wunused + +colorize: colorize.o + $(CC) -o $@ $< + +colorize.o: colorize.c + $(CC) $(CFLAGS) -c $< -DCFLAGS="$(CFLAGS)" + +check: + perl ./test.pl + +clean: + [ -e colorize.o ] && rm colorize.o; exit 0 diff --git a/colorize.c b/colorize.c new file mode 100644 index 0000000..7e9edcb --- /dev/null +++ b/colorize.c @@ -0,0 +1,730 @@ +/* + * colorize - Read text from standard input stream or file and print + * it colorized through use of ANSI escape sequences + * + * Copyright (c) 2011-2012 Steven Schubiger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#define _POSIX_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define str(arg) #arg +#define to_str(arg) str(arg) + +#define streq(s1, s2) (strcmp (s1, s2) == 0) + +#if !defined BUF_SIZE || BUF_SIZE <= 0 +# undef BUF_SIZE +# define BUF_SIZE 4096 + 1 +#endif + +#define LF 0x01 +#define CR 0x02 + +#define SKIP_LINE_ENDINGS(flags) (((flags) & CR) && ((flags) & LF) ? 2 : 1) + +#define STACK_VAR(ptr) do { \ + stack_var (&vars_list, &stacked_vars, stacked_vars, ptr); \ +} while (false); + +#define RELEASE_VAR(ptr) do { \ + release_var (vars_list, stacked_vars, (void **)&ptr); \ +} while (false); + +#define ABORT_TRACE() \ + fprintf (stderr, "aborting in source file %s, line %d\n", __FILE__, __LINE__); \ + abort (); \ + +#define CHECK_COLORS_RANDOM(color1, color2) \ + streq (color_names[color1]->name, "random") \ + && (streq (color_names[color2]->name, "none") \ + || streq (color_names[color2]->name, "default")) \ + +#define VERSION "0.47" + +typedef unsigned short bool; + +enum { false, true }; + +struct color_name { + char *name; + char *orig; +}; + +static struct color_name *color_names[3] = { NULL, NULL, NULL }; + +struct color { + const char *name; + const char *code; +}; + +static const struct color fg_colors[] = { + { "none", NULL }, + { "black", "30m" }, + { "red", "31m" }, + { "green", "32m" }, + { "yellow", "33m" }, + { "blue", "34m" }, + { "cyan", "35m" }, + { "magenta", "36m" }, + { "white", "37m" }, + { "default", "39m" }, +}; +static const struct color bg_colors[] = { + { "none", NULL }, + { "black", "40m" }, + { "red", "41m" }, + { "green", "42m" }, + { "yellow", "43m" }, + { "blue", "44m" }, + { "cyan", "45m" }, + { "magenta", "46m" }, + { "white", "47m" }, + { "default", "49m" }, +}; + +enum fmts { + FMT_GENERIC, + FMT_COLOR, + FMT_RANDOM, + FMT_ERROR, + FMT_FILE +}; +static const char *formats[] = { + "%s", /* generic */ + "%s color '%s' %s", /* color */ + "%s color '%s' %s '%s'", /* random */ + "less than %d bytes %s", /* error */ + "%s: %s", /* file */ +}; + +enum { FOREGROUND, BACKGROUND }; + +static const struct { + struct color const *entries; + unsigned int count; + const char *desc; +} tables[] = { + { fg_colors, sizeof (fg_colors) / sizeof (struct color), "foreground" }, + { bg_colors, sizeof (bg_colors) / sizeof (struct color), "background" }, +}; + +enum stream_mode { SCAN_FIRST = 1, SCAN_ALWAYS }; + +struct ending { + unsigned int flags; + const char newline[3]; +}; + +static const struct ending endings[] = { + { CR & LF, "\r\n" }, + { CR, "\r" }, + { LF, "\n" }, +}; + +static FILE *stream = NULL; + +static unsigned int stacked_vars = 0; +static void **vars_list = NULL; + +static char *exclude = NULL; + +static const char *program_name; + +static void print_help (void); +static void print_version (void); +static void cleanup (void); +static void free_color_names (struct color_name **); +static void process_options (unsigned int, char **, bool *, const struct color **, const char **, FILE **); +static void read_print_stream (bool, const struct color **, const char *, FILE *, enum stream_mode); +static void find_color_entries (struct color_name **, const struct color **); +static void find_color_entry (const char *const, unsigned int, const struct color **); +static void print_line (const struct color **, bool, const char * const, unsigned int); +static char *xstrdup (const char *); +static void vfprintf_fail (const char *, ...); +static void stack_var (void ***, unsigned int *, unsigned int, void *); +static void release_var (void **, unsigned int, void **); + +extern char *optarg; +extern int optind; + +int +main (int argc, char **argv) +{ + unsigned int arg_cnt = 0; + + bool invalid_opt = false; + + int opt; + struct option long_opts[] = { + { "exclude-random", required_argument, NULL, 'e' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + { 0, 0, 0, 0 }, + }; + + bool bold = false; + + const struct color *colors[2] = { + NULL, /* foreground */ + NULL, /* background */ + }; + + const char *file; + + enum stream_mode mode = SCAN_FIRST; + + program_name = argv[0]; + atexit (cleanup); + + setvbuf (stdout, NULL, _IOLBF, 0); + + while ((opt = getopt_long (argc, argv, "hv", long_opts, NULL)) != -1) + { + switch (opt) + { + case 'e': { + char *p; + exclude = xstrdup (optarg); + STACK_VAR (exclude); + for (p = exclude; *p; p++) + *p = tolower (*p); + if (streq (exclude, "random")) + vfprintf_fail (formats[FMT_GENERIC], "--exclude-random switch must be provided a color"); + break; + } + case 'h': + print_help (); + exit (EXIT_SUCCESS); + case 'v': + print_version (); + exit (EXIT_SUCCESS); + case '?': + invalid_opt = true; + break; + default: /* never reached */ + ABORT_TRACE (); + } + } + + arg_cnt = argc - optind; + + if (arg_cnt == 0 || arg_cnt > 2 || invalid_opt) + { + print_help (); + exit (EXIT_FAILURE); + } + + process_options (arg_cnt, &argv[optind], &bold, colors, &file, &stream); + read_print_stream (bold, colors, file, stream, mode); + + RELEASE_VAR (exclude); + + exit (EXIT_SUCCESS); +} + +static void +print_help (void) +{ + unsigned int i; + + printf ("Usage: %s (foreground) OR (foreground)/(background) [-|file]\n\n", program_name); + printf ("\tColors (foreground) (background)\n"); + for (i = 0; i < tables[FOREGROUND].count; i++) + { + const struct color *entry = &tables[FOREGROUND].entries[i]; + const char *name = entry->name; + const char *code = entry->code; + if (code) + printf ("\t\t{\033[%s#\033[0m} [%c%c]%s%*s%s\n", + code, toupper (*name), *name, name + 1, 10 - (int)strlen (name), " ", name); + else + printf ("\t\t{-} %s%*s%s\n", name, 13 - (int)strlen (name), " ", name); + } + printf ("\t\t{*} [Rr]%s%*s%s [--exclude-random=]\n", "andom", 10 - (int)strlen ("random"), " ", "random"); + + printf ("\n\tFirst character of color name in upper case denotes increased intensity,\n"); + printf ("\twhereas for lower case colors will be of normal intensity.\n"); + + printf ("\n\tOptions\n"); + printf ("\t\t-h, --help\n"); + printf ("\t\t-v, --version\n\n"); +} + +static void +print_version (void) +{ + const char *c_flags; + printf ("%s v%s (compiled at %s, %s)\n", "colorize", VERSION, __DATE__, __TIME__); +#ifdef CFLAGS + c_flags = to_str (CFLAGS); +#else + c_flags = "unknown"; +#endif + printf ("Compiler flags: %s\n", c_flags); + printf ("Buffer size: %d bytes\n", BUF_SIZE - 1); +} + +static void +cleanup (void) +{ + free_color_names (color_names); + + if (stream && fileno (stream) != STDIN_FILENO) + fclose (stream); + + if (vars_list) + { + unsigned int i; + for (i = 0; i < stacked_vars; i++) + if (vars_list[i]) + { + free (vars_list[i]); + vars_list[i] = NULL; + } + free (vars_list); + vars_list = NULL; + } +} + +static void +free_color_names (struct color_name **color_names) +{ + unsigned int i; + for (i = 0; color_names[i]; i++) + { + free (color_names[i]->name); + color_names[i]->name = NULL; + free (color_names[i]->orig); + color_names[i]->orig = NULL; + free (color_names[i]); + color_names[i] = NULL; + } +} + +static void +process_options (unsigned int arg_cnt, char **option_strings, bool *bold, const struct color **colors, const char **file, FILE **stream) +{ + int ret; + unsigned int index; + char *color, *p, *str; + struct stat sb; + + const char *color_string = arg_cnt >= 1 ? option_strings[0] : NULL; + const char *file_string = arg_cnt == 2 ? option_strings[1] : NULL; + + assert (color_string); + + if (streq (color_string, "-")) + { + if (file_string) + vfprintf_fail (formats[FMT_GENERIC], "hyphen cannot be used as color string"); + else + vfprintf_fail (formats[FMT_GENERIC], "hyphen must be preceeded by color string"); + } + + ret = stat (color_string, &sb); + + /* Ensure that we don't fail if there's a file with one or more + color names in its path. */ + if (ret != -1) + { + bool have_file; + unsigned int c; + const char *color = color_string; + + for (c = 1; c <= 2 && *color; c++) + { + bool matched = false; + unsigned int i; + for (i = 0; i < tables[FOREGROUND].count; i++) + { + const struct color *entry = &tables[FOREGROUND].entries[i]; + char *p; + if ((p = strstr (color, entry->name)) && p == color) + { + color = p + strlen (entry->name); + matched = true; + break; + } + } + if (matched && *color == '/' && *(color + 1)) + color++; + else + break; + } + + have_file = (*color != '\0'); + + if (have_file) + vfprintf_fail (formats[FMT_GENERIC], "file must be preceeded by color string"); + } + + if ((p = strchr (color_string, '/'))) + { + if (p == color_string) + vfprintf_fail (formats[FMT_GENERIC], "foreground color missing"); + else if (p == color_string + strlen (color_string) - 1) + vfprintf_fail (formats[FMT_GENERIC], "background color missing"); + else if (strchr (++p, '/')) + vfprintf_fail (formats[FMT_GENERIC], "one color pair allowed only"); + } + + str = xstrdup (color_string); + STACK_VAR (str); + + for (index = 0, color = str; *color; index++, color = p) + { + char *ch, *sep; + if ((sep = strchr (color, '/'))) + { + *sep = '\0'; + p = sep + 1; + } + else + p = color + strlen (color); + + for (ch = color; *ch; ch++) + if (!isalpha (*ch)) + vfprintf_fail (formats[FMT_COLOR], tables[index].desc, color, "cannot be made of non-alphabetic characters"); + + for (ch = color + 1; *ch; ch++) + if (!islower (*ch)) + vfprintf_fail (formats[FMT_COLOR], tables[index].desc, color, "cannot be in mixed lower/upper case"); + + if (streq (color, "None")) + vfprintf_fail (formats[FMT_COLOR], tables[index].desc, color, "cannot be bold"); + + if (isupper (*color)) + { + switch (index) + { + case FOREGROUND: + *bold = true; + break; + case BACKGROUND: + vfprintf_fail (formats[FMT_COLOR], tables[BACKGROUND].desc, color, "cannot be bold"); + break; + default: /* never reached */ + ABORT_TRACE (); + } + } + + color_names[index] = malloc (sizeof (struct color_name)); + + color_names[index]->orig = xstrdup (color); + + for (ch = color; *ch; ch++) + *ch = tolower (*ch); + + color_names[index]->name = xstrdup (color); + } + + RELEASE_VAR (str); + + assert (color_names[FOREGROUND]); + + if (color_names[BACKGROUND]) + { + unsigned int i; + unsigned int color_sets[2][2] = { { FOREGROUND, BACKGROUND }, { BACKGROUND, FOREGROUND } }; + for (i = 0; i < 2; i++) + { + unsigned int color1 = color_sets[i][0]; + unsigned int color2 = color_sets[i][1]; + if (CHECK_COLORS_RANDOM (color1, color2)) + vfprintf_fail (formats[FMT_RANDOM], tables[color1].desc, color_names[color1]->orig, "cannot be combined with", color_names[color2]->orig); + } + } + + find_color_entries (color_names, colors); + free_color_names (color_names); + + if (!colors[FOREGROUND]->code && colors[BACKGROUND] && colors[BACKGROUND]->code) + find_color_entry ("default", FOREGROUND, colors); + + if (file_string) + { + if (streq (file_string, "-")) + *stream = stdin; + else + { + FILE *s; + const char *file = file_string; + struct stat sb; + int errno, ret; + + errno = 0; + ret = stat (file, &sb); + + if (ret == -1) + vfprintf_fail (formats[FMT_FILE], file, strerror (errno)); + + if (!(S_ISREG (sb.st_mode) || S_ISLNK (sb.st_mode) || S_ISFIFO (sb.st_mode))) + vfprintf_fail (formats[FMT_FILE], file, "unrecognized file type"); + + errno = 0; + + s = fopen (file, "r"); + if (!s) + vfprintf_fail (formats[FMT_FILE], file, strerror (errno)); + *stream = s; + } + *file = file_string; + } + else + { + *stream = stdin; + *file = "stdin"; + } + + assert (*stream); +} + +static void +read_print_stream (bool bold, const struct color **colors, const char *file, FILE *stream, enum stream_mode mode) +{ + char buf[BUF_SIZE]; + unsigned int flags = 0; + bool first = false, always = false; + + switch (mode) + { + case SCAN_FIRST: + first = true; + break; + case SCAN_ALWAYS: + always = true; + break; + default: /* never reached */ + ABORT_TRACE (); + } + + while (!feof (stream)) + { + size_t bytes_read; + char *eol; + const char *line; + memset (buf, '\0', BUF_SIZE); + bytes_read = fread (buf, 1, BUF_SIZE - 1, stream); + if (bytes_read != (BUF_SIZE - 1) && ferror (stream)) + vfprintf_fail (formats[FMT_ERROR], BUF_SIZE - 1, "read"); + line = buf; + LOOP: while ((eol = strpbrk (line, "\n\r"))) + { + char *p; + if (first || always) + { + first = false; + flags &= ~(CR|LF); + if (*eol == '\r') + { + flags |= CR; + if (*(eol + 1) == '\n') + flags |= LF; + } + else if (*eol == '\n') + flags |= LF; + else + vfprintf_fail (formats[FMT_FILE], file, "unrecognized line ending"); + } + if (always) + p = eol + SKIP_LINE_ENDINGS (flags); + else /* first */ + { + unsigned int i; + unsigned int count = sizeof (endings) / sizeof (struct ending); + for (i = 0; i < count; i++) + { + if (flags & endings[i].flags) + { + char *p; + if ((p = strstr (eol, endings[i].newline)) && p == eol) + break; + else + { + always = true; + goto LOOP; + } + } + } + p = eol + SKIP_LINE_ENDINGS (flags); + } + *eol = '\0'; + print_line (colors, bold, line, flags); + line = p; + } + print_line (colors, bold, line, 0); + } +} + +static void +find_color_entries (struct color_name **color_names, const struct color **colors) +{ + struct timeval tv; + unsigned int index; + + /* randomness */ + gettimeofday (&tv, NULL); + srand (tv.tv_usec * tv.tv_sec); + + for (index = 0; color_names[index]; index++) + { + const char *color_name = color_names[index]->name; + + const unsigned int count = tables[index].count; + const struct color *const color_entries = tables[index].entries; + + if (streq (color_name, "random")) + { + bool excludable; + unsigned int i; + do { + excludable = false; + i = rand() % (count - 2) + 1; /* omit color none and default */ + switch (index) + { + case FOREGROUND: + /* --exclude-random */ + if (exclude && streq (exclude, color_entries[i].name)) + excludable = true; + else if (color_names[BACKGROUND] && streq (color_names[BACKGROUND]->name, color_entries[i].name)) + excludable = true; + break; + case BACKGROUND: + if (streq (colors[FOREGROUND]->name, color_entries[i].name)) + excludable = true; + break; + default: /* never reached */ + ABORT_TRACE (); + } + } while (excludable); + colors[index] = (struct color *)&color_entries[i]; + } + else + find_color_entry (color_name, index, colors); + } +} + +static void +find_color_entry (const char *const color_name, unsigned int index, const struct color **colors) +{ + bool found = false; + unsigned int i; + + const unsigned int count = tables[index].count; + const struct color *const color_entries = tables[index].entries; + + for (i = 0; i < count; i++) + if (streq (color_name, color_entries[i].name)) + { + colors[index] = (struct color *)&color_entries[i]; + found = true; + break; + } + if (!found) + vfprintf_fail (formats[FMT_COLOR], tables[index].desc, color_name, "not recognized"); +} + +static void +print_line (const struct color **colors, bool bold, const char *const line, unsigned int flags) +{ + if (colors[BACKGROUND] && colors[BACKGROUND]->code) + printf ("\033[%s", colors[BACKGROUND]->code); + if (colors[FOREGROUND]->code) + printf ("\033[%s%s%s\033[0m", bold ? "1;" : "", colors[FOREGROUND]->code, line); + else + printf (formats[FMT_GENERIC], line); + if (flags & CR) + putchar ('\r'); + if (flags & LF) + putchar ('\n'); +} + +static char * +xstrdup (const char *str) +{ + const unsigned int len = strlen (str) + 1; + char *p = malloc (len); + assert (p != NULL); + strncpy (p, str, len); + return p; +} + +static void +vfprintf_fail (const char *fmt, ...) +{ + va_list ap; + fprintf (stderr, "%s: ", program_name); + va_start (ap, fmt); + vfprintf (stderr, fmt, ap); + va_end (ap); + fprintf (stderr, "\n"); + exit (EXIT_FAILURE); +} + +static void +stack_var (void ***list, unsigned int *stacked, unsigned int index, void *ptr) +{ + /* nothing to stack */ + if (ptr == NULL) + return; + if (!*list) + *list = malloc (sizeof (void *)); + else + { + unsigned int i; + for (i = 0; i < *stacked; i++) + if (!(*list)[i]) + { + (*list)[i] = ptr; + return; /* reused */ + } + *list = realloc (*list, (*stacked + 1) * sizeof (void *)); + } + (*list)[index] = ptr; + (*stacked)++; +} + +static void +release_var (void **list, unsigned int stacked, void **ptr) +{ + unsigned int i; + /* nothing to release */ + if (*ptr == NULL) + return; + for (i = 0; i < stacked; i++) + if (list[i] == *ptr) + { + free (*ptr); + *ptr = NULL; + list[i] = NULL; + return; + } +} diff --git a/test.pl b/test.pl new file mode 100755 index 0000000..d8fc52a --- /dev/null +++ b/test.pl @@ -0,0 +1,91 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use constant true => 1; + +use File::Temp qw(tempfile tmpnam); +use Test::More tests => 9; + +my $BUF_SIZE = 1024; +my $source = 'colorize.c'; +my $warning_flags = '-Wall -Wextra -Wformat -Wswitch-default -Wuninitialized -Wunused'; + +my $write_to_tmpfile = sub +{ + my ($content) = @_; + + my ($fh, $tmpfile) = tempfile(UNLINK => true); + print {$fh} $content; + close($fh); + + return $tmpfile; +}; + +my $program; + +SKIP: { + skip "$source does not exist", 9 unless -e $source; + + $program = tmpnam(); + + skip 'compiling failed', 9 unless system("gcc -DTEST -DBUF_SIZE=$BUF_SIZE $warning_flags -o $program $source") == 0; + + is(system("$program --help >/dev/null 2>&1"), 0, 'exit value for help screen'); + + is(qx(echo "hello world" | $program none/none), "hello world\n", 'line read from stdin with newline'); + is(qx(echo -n "hello world" | $program none/none), "hello world", 'line read from stdin without newline'); + + my $text = do { local $/; }; + + my $infile1 = $write_to_tmpfile->($text); + + is_deeply([split /\n/, qx(cat $infile1 | $program none/none)], [split /\n/, $text], 'text read from stdin'); + is_deeply([split /\n/, qx($program none/none $infile1)], [split /\n/, $text], 'text read from file'); + + my $repeated = join "\n", ($text) x 7; + my $infile2 = $write_to_tmpfile->($repeated); + + is_deeply([split /\n/, qx(cat $infile2 | $program none/none)], [split /\n/, $repeated], "read ${\length $repeated} bytes (BUF_SIZE=$BUF_SIZE)"); + + is(qx(echo -n "hello\nworld\r\n" | $program none/none), "hello\nworld\r\n", 'stream mode'); + + is(system("echo \"hello world\" | $program random --exclude-random=black >/dev/null 2>&1"), 0, 'switch exclude-random'); + + skip 'valgrind not found', 1 unless system('which valgrind >/dev/null 2>&1') == 0; + like(qx(valgrind $program none/none $infile1 2>&1 >/dev/null), qr/no leaks are possible/, 'valgrind memleaks'); + + print <<'EOT'; +Colors +====== +EOT + foreach my $color (qw(none black red green yellow blue cyan magenta white default random)) { + system("echo $color | $program $color"); + next if $color eq 'none'; + my $bold_color = ucfirst $color; + system("echo $bold_color | $program $bold_color"); + } +}; + +unlink $program if defined $program; + +__DATA__ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus urna mauris, ultricies faucibus placerat sit amet, rutrum eu +nisi. Quisque dictum turpis non augue iaculis tincidunt nec a arcu. Donec euismod sapien ac dui blandit et adipiscing risus +semper. Sed ornare ligula magna, vitae molestie eros. Praesent ligula est, euismod a luctus non, porttitor quis nunc. Fusce vel +imperdiet turpis. Proin vitae mauris neque, fringilla vestibulum sapien. Pellentesque vitae nibh ipsum, non cursus diam. Cras +vitae ligula mauris. Etiam tortor enim, varius nec adipiscing sed, lobortis et quam. Quisque convallis, diam sagittis adipiscing +adipiscing, mi nibh fermentum sapien, et iaculis nisi sem sit amet odio. Cras a tortor at nibh tristique vehicula dapibus eu velit. + +Vivamus porttitor purus eget leo suscipit sed posuere ligula gravida. In mollis velit quis leo pharetra gravida. Ut libero nisi, +elementum sed varius tincidunt, hendrerit ut dui. Duis sit amet ante eget velit dictum ultrices. Nulla tempus, lacus eu dignissim +feugiat, turpis mauris volutpat urna, quis commodo lorem augue id justo. Aenean consequat interdum sapien, sit amet +imperdiet ante dapibus at. Pellentesque viverra sagittis tincidunt. Quisque rhoncus varius magna, sit amet rutrum arcu +tincidunt eget. Etiam a lacus nec mauris interdum luctus sed in lacus. Ut pulvinar, augue at dictum blandit, nisl massa pretium +ligula, in iaculis nulla nisi iaculis nunc. + +Vivamus id eros nunc. Cras facilisis iaculis ante sit amet consequat. Nunc vehicula imperdiet sem, ac vehicula neque +condimentum sed. Phasellus metus lacus, molestie ullamcorper imperdiet in, condimentum ut tellus. Nullam dignissim dui ut +enim ullamcorper in tempus risus posuere. Ut volutpat enim eleifend diam convallis tristique. Proin porttitor augue sed sapien +sagittis quis facilisis purus sodales. Integer auctor dolor rhoncus nisl consequat adipiscing. Aliquam eget ante sit amet quam +porta eleifend. -- cgit v1.2.3