Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit. Mandatory ASCII chicken: <")3

  • Loading branch information...
commit 96e7db3e482482f6ef4dc95b88547dd046d0725b 0 parents
@vmg authored
3  .gitmodules
@@ -0,0 +1,3 @@
+[submodule "upskirt"]
+ path = upskirt
+ url = git://github.com/tanoku/upskirt.git
13 COPYING
@@ -0,0 +1,13 @@
+Copyright (c) 2011, Vicent Marti
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
0  README.markdown
No changes.
71 Rakefile
@@ -0,0 +1,71 @@
+require 'date'
+require 'rake/clean'
+require 'rake/extensiontask'
+require 'digest/md5'
+
+task :default => :test
+
+# ==========================================================
+# Ruby Extension
+# ==========================================================
+
+Rake::ExtensionTask.new('rinku')
+
+# ==========================================================
+# Testing
+# ==========================================================
+
+require 'rake/testtask'
+Rake::TestTask.new('test') do |t|
+ t.test_files = FileList['test/*_test.rb']
+ t.ruby_opts += ['-rubygems'] if defined? Gem
+end
+task 'test' => [:compile]
+
+# PACKAGING =================================================================
+
+require 'rubygems'
+$spec = eval(File.read('rinku.gemspec'))
+
+def package(ext='')
+ "pkg/rinku-#{$spec.version}" + ext
+end
+
+desc 'Build packages'
+task :package => package('.gem')
+
+desc 'Build and install as local gem'
+task :install => package('.gem') do
+ sh "gem install #{package('.gem')}"
+end
+
+desc 'Update the gemspec'
+task :update_gem => file('rinku.gemspec')
+
+directory 'pkg/'
+
+file package('.gem') => %w[pkg/ rinku.gemspec] + $spec.files do |f|
+ sh "gem build rinku.gemspec"
+ mv File.basename(f.name), f.name
+end
+
+# GEMSPEC HELPERS ==========================================================
+
+desc 'Gather required Upskirt sources into extension directory'
+task :gather => 'upskirt/src/markdown.h' do |t|
+ files =
+ FileList[
+ 'upskirt/src/{buffer,autolink}.h',
+ 'upskirt/src/{buffer,autolink}.c',
+ 'upskirt/html/html_autolink.c'
+ ]
+ cp files, 'ext/rinku/',
+ :preserve => true,
+ :verbose => true
+end
+
+file 'upskirt/src/markdown.h' do |t|
+ abort "The Upskirt submodule is required."
+end
+
+
1  VERSION
@@ -0,0 +1 @@
+0.1.0
239 ext/rinku/autolink.c
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "buffer.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+int
+is_safe_link(const char *link, size_t link_len)
+{
+ static const size_t valid_uris_count = 4;
+ static const char *valid_uris[] = {
+ "http://", "https://", "ftp://", "mailto://"
+ };
+
+ size_t i;
+
+ for (i = 0; i < valid_uris_count; ++i) {
+ size_t len = strlen(valid_uris[i]);
+
+ if (link_len > len &&
+ strncasecmp(link, valid_uris[i], len) == 0 &&
+ isalnum(link[len]))
+ return 1;
+ }
+
+ return 0;
+}
+
+static size_t
+autolink_delim(char *data, size_t link_end, size_t offset, size_t size)
+{
+ char cclose, copen = 0;
+
+ while (link_end > 0) {
+ if (strchr("?!.,", data[link_end - 1]) != NULL)
+ link_end--;
+
+ else if (data[link_end - 1] == ';') {
+ size_t new_end = link_end - 2;
+
+ while (new_end > 0 && isalpha(data[new_end]))
+ new_end--;
+
+ if (new_end < link_end - 2 && data[new_end] == '&')
+ link_end = new_end;
+ else
+ link_end--;
+ }
+
+ else if (data[link_end - 1] == '>') {
+ while (link_end > 0 && data[link_end] != '<')
+ link_end--;
+ }
+ else break;
+ }
+
+ if (link_end == 0)
+ return 0;
+
+ cclose = data[link_end - 1];
+
+ switch (cclose) {
+ case '"': copen = '"'; break;
+ case '\'': copen = '\''; break;
+ case ')': copen = '('; break;
+ case ']': copen = '['; break;
+ case '}': copen = '{'; break;
+ }
+
+ if (copen != 0) {
+ size_t closing = 0;
+ size_t opening = 0;
+ size_t i = 0;
+
+ /* Try to close the final punctuation sign in this same line;
+ * if we managed to close it outside of the URL, that means that it's
+ * not part of the URL. If it closes inside the URL, that means it
+ * is part of the URL.
+ *
+ * Examples:
+ *
+ * foo http://www.pokemon.com/Pikachu_(Electric) bar
+ * => http://www.pokemon.com/Pikachu_(Electric)
+ *
+ * foo (http://www.pokemon.com/Pikachu_(Electric)) bar
+ * => http://www.pokemon.com/Pikachu_(Electric)
+ *
+ * foo http://www.pokemon.com/Pikachu_(Electric)) bar
+ * => http://www.pokemon.com/Pikachu_(Electric))
+ *
+ * (foo http://www.pokemon.com/Pikachu_(Electric)) bar
+ * => foo http://www.pokemon.com/Pikachu_(Electric)
+ */
+
+ while (i < link_end) {
+ if (data[i] == copen)
+ opening++;
+ else if (data[i] == cclose)
+ closing++;
+
+ i++;
+ }
+
+ if (closing != opening)
+ link_end--;
+ }
+
+ return link_end;
+}
+
+size_t
+ups_autolink__www(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size)
+{
+ size_t link_end;
+ int np = 0;
+
+ if (offset > 0 && !ispunct(data[-1]) && !isspace(data[-1]))
+ return 0;
+
+ if (size < 4 || memcmp(data, "www.", STRLEN("www.")) != 0)
+ return 0;
+
+ link_end = 0;
+ while (link_end < size && !isspace(data[link_end])) {
+ if (data[link_end] == '.')
+ np++;
+
+ link_end++;
+ }
+
+ if (np < 2)
+ return 0;
+
+ link_end = autolink_delim(data, link_end, offset, size);
+
+ if (link_end == 0)
+ return 0;
+
+ bufput(link, data, link_end);
+ *rewind_p = 0;
+
+ return (int)link_end;
+}
+
+size_t
+ups_autolink__email(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size)
+{
+ size_t link_end, rewind;
+ int nb = 0, np = 0;
+
+ for (rewind = 0; rewind < offset; ++rewind) {
+ char c = data[-rewind - 1];
+
+ if (isalnum(c))
+ continue;
+
+ if (strchr(".+-_", c) != NULL)
+ continue;
+
+ break;
+ }
+
+ if (rewind == 0)
+ return 0;
+
+ for (link_end = 0; link_end < size; ++link_end) {
+ char c = data[link_end];
+
+ if (isalnum(c))
+ continue;
+
+ if (c == '@')
+ nb++;
+ else if (c == '.' && link_end < size - 1)
+ np++;
+ else if (c != '-' && c != '_')
+ break;
+ }
+
+ if (link_end < 2 || nb != 1 || np == 0)
+ return 0;
+
+ link_end = autolink_delim(data, link_end, offset, size);
+
+ if (link_end == 0)
+ return 0;
+
+ bufput(link, data - rewind, link_end + rewind);
+ *rewind_p = rewind;
+
+ return link_end;
+}
+
+size_t
+ups_autolink__url(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size)
+{
+ size_t link_end, rewind = 0;
+
+ if (size < 4 || data[1] != '/' || data[2] != '/')
+ return 0;
+
+ while (rewind < offset && isalpha(data[-rewind - 1]))
+ rewind++;
+
+ if (!is_safe_link(data - rewind, size + rewind))
+ return 0;
+
+ link_end = 0;
+ while (link_end < size && !isspace(data[link_end]))
+ link_end++;
+
+ link_end = autolink_delim(data, link_end, offset, size);
+
+ if (link_end == 0)
+ return 0;
+
+ bufput(link, data - rewind, link_end + rewind);
+ *rewind_p = rewind;
+
+ return link_end;
+}
+
39 ext/rinku/autolink.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef UPSKIRT_AUTOLINK_H
+#define UPSKIRT_AUTOLINK_H_H
+
+#include "buffer.h"
+
+typedef enum {
+ AUTOLINK_URLS = (1 << 0),
+ AUTOLINK_EMAILS = (1 << 1),
+ AUTOLINK_ALL = AUTOLINK_URLS|AUTOLINK_EMAILS
+} autolink_mode;
+
+extern size_t
+ups_autolink__www(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size);
+
+extern size_t
+ups_autolink__email(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size);
+
+extern size_t
+ups_autolink__url(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size);
+
+#endif
+
+/* vim: set filetype=c: */
323 ext/rinku/buffer.c
@@ -0,0 +1,323 @@
+/* buffer.c - automatic buffer structure */
+
+/*
+ * Copyright (c) 2008, Natacha Porté
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * COMPILE TIME OPTIONS
+ *
+ * BUFFER_STATS • if defined, stats are kept about memory usage
+ */
+
+#define BUFFER_STDARG
+#define BUFFER_MAX_ALLOC_SIZE (1024 * 1024 * 16) //16mb
+
+#include "buffer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+/********************
+ * GLOBAL VARIABLES *
+ ********************/
+
+#ifdef BUFFER_STATS
+long buffer_stat_nb = 0;
+size_t buffer_stat_alloc_bytes = 0;
+#endif
+
+
+/***************************
+ * STATIC HELPER FUNCTIONS *
+ ***************************/
+
+/* lower • retruns the lower-case variant of the input char */
+static char
+lower(char c) {
+ return (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c; }
+
+
+
+/********************
+ * BUFFER FUNCTIONS *
+ ********************/
+
+/* bufcasecmp • case-insensitive buffer comparison */
+int
+bufcasecmp(const struct buf *a, const struct buf *b) {
+ size_t i = 0;
+ size_t cmplen;
+ if (a == b) return 0;
+ if (!a) return -1; else if (!b) return 1;
+ cmplen = (a->size < b->size) ? a->size : b->size;
+ while (i < cmplen && lower(a->data[i]) == lower(b->data[i])) ++i;
+ if (i < a->size) {
+ if (i < b->size) return lower(a->data[i]) - lower(b->data[i]);
+ else return 1; }
+ else { if (i < b->size) return -1;
+ else return 0; } }
+
+
+/* bufcmp • case-sensitive buffer comparison */
+int
+bufcmp(const struct buf *a, const struct buf *b) {
+ size_t i = 0;
+ size_t cmplen;
+ if (a == b) return 0;
+ if (!a) return -1; else if (!b) return 1;
+ cmplen = (a->size < b->size) ? a->size : b->size;
+ while (i < cmplen && a->data[i] == b->data[i]) ++i;
+ if (i < a->size) {
+ if (i < b->size) return a->data[i] - b->data[i];
+ else return 1; }
+ else { if (i < b->size) return -1;
+ else return 0; } }
+
+
+/* bufcmps • case-sensitive comparison of a string to a buffer */
+int
+bufcmps(const struct buf *a, const char *b) {
+ const size_t len = strlen(b);
+ size_t cmplen = len;
+ int r;
+ if (!a || !a->size) return b ? 0 : -1;
+ if (len < a->size) cmplen = a->size;
+ r = strncmp(a->data, b, cmplen);
+ if (r) return r;
+ else if (a->size == len) return 0;
+ else if (a->size < len) return -1;
+ else return 1; }
+
+int
+bufprefix(const struct buf *buf, const char *prefix)
+{
+ size_t i;
+
+ for (i = 0; i < buf->size; ++i) {
+ if (prefix[i] == 0)
+ return 0;
+
+ if (buf->data[i] != prefix[i])
+ return buf->data[i] - prefix[i];
+ }
+
+ return 0;
+}
+
+
+/* bufdup • buffer duplication */
+struct buf *
+bufdup(const struct buf *src, size_t dupunit) {
+ size_t blocks;
+ struct buf *ret;
+ if (src == 0) return 0;
+ ret = malloc(sizeof (struct buf));
+ if (ret == 0) return 0;
+ ret->unit = dupunit;
+ ret->size = src->size;
+ ret->ref = 1;
+ if (!src->size) {
+ ret->asize = 0;
+ ret->data = 0;
+ return ret; }
+ blocks = (src->size + dupunit - 1) / dupunit;
+ ret->asize = blocks * dupunit;
+ ret->data = malloc(ret->asize);
+ if (ret->data == 0) {
+ free(ret);
+ return 0; }
+ memcpy(ret->data, src->data, src->size);
+#ifdef BUFFER_STATS
+ buffer_stat_nb += 1;
+ buffer_stat_alloc_bytes += ret->asize;
+#endif
+ return ret; }
+
+/* bufgrow • increasing the allocated size to the given value */
+int
+bufgrow(struct buf *buf, size_t neosz) {
+ size_t neoasz;
+ void *neodata;
+ if (!buf || !buf->unit || neosz > BUFFER_MAX_ALLOC_SIZE) return 0;
+ if (buf->asize >= neosz) return 1;
+ neoasz = buf->asize + buf->unit;
+ while (neoasz < neosz) neoasz += buf->unit;
+ neodata = realloc(buf->data, neoasz);
+ if (!neodata) return 0;
+#ifdef BUFFER_STATS
+ buffer_stat_alloc_bytes += (neoasz - buf->asize);
+#endif
+ buf->data = neodata;
+ buf->asize = neoasz;
+ return 1; }
+
+
+/* bufnew • allocation of a new buffer */
+struct buf *
+bufnew(size_t unit) {
+ struct buf *ret;
+ ret = malloc(sizeof (struct buf));
+ if (ret) {
+#ifdef BUFFER_STATS
+ buffer_stat_nb += 1;
+#endif
+ ret->data = 0;
+ ret->size = ret->asize = 0;
+ ret->ref = 1;
+ ret->unit = unit; }
+ return ret; }
+
+
+/* bufnullterm • NUL-termination of the string array (making a C-string) */
+void
+bufnullterm(struct buf *buf) {
+ if (!buf || !buf->unit) return;
+ if (buf->size < buf->asize && buf->data[buf->size] == 0) return;
+ if (buf->size + 1 <= buf->asize || bufgrow(buf, buf->size + 1))
+ buf->data[buf->size] = 0; }
+
+
+/* bufprintf • formatted printing to a buffer */
+void
+bufprintf(struct buf *buf, const char *fmt, ...) {
+ va_list ap;
+ if (!buf || !buf->unit) return;
+ va_start(ap, fmt);
+ vbufprintf(buf, fmt, ap);
+ va_end(ap); }
+
+
+/* bufput • appends raw data to a buffer */
+void
+bufput(struct buf *buf, const void *data, size_t len) {
+ if (!buf) return;
+ if (buf->size + len > buf->asize && !bufgrow(buf, buf->size + len))
+ return;
+ memcpy(buf->data + buf->size, data, len);
+ buf->size += len; }
+
+
+/* bufputs • appends a NUL-terminated string to a buffer */
+void
+bufputs(struct buf *buf, const char *str) {
+ bufput(buf, str, strlen (str)); }
+
+
+/* bufputc • appends a single char to a buffer */
+void
+bufputc(struct buf *buf, char c) {
+ if (!buf) return;
+ if (buf->size + 1 > buf->asize && !bufgrow(buf, buf->size + 1))
+ return;
+ buf->data[buf->size] = c;
+ buf->size += 1; }
+
+
+/* bufrelease • decrease the reference count and free the buffer if needed */
+void
+bufrelease(struct buf *buf) {
+ if (!buf) return;
+ buf->ref -= 1;
+ if (buf->ref == 0) {
+#ifdef BUFFER_STATS
+ buffer_stat_nb -= 1;
+ buffer_stat_alloc_bytes -= buf->asize;
+#endif
+ free(buf->data);
+ free(buf); } }
+
+
+/* bufreset • frees internal data of the buffer */
+void
+bufreset(struct buf *buf) {
+ if (!buf) return;
+#ifdef BUFFER_STATS
+ buffer_stat_alloc_bytes -= buf->asize;
+#endif
+ free(buf->data);
+ buf->data = 0;
+ buf->size = buf->asize = 0; }
+
+
+/* bufset • safely assigns a buffer to another */
+void
+bufset(struct buf **dest, struct buf *src) {
+ if (src) {
+ if (!src->asize) src = bufdup(src, 1);
+ else src->ref += 1; }
+ bufrelease(*dest);
+ *dest = src; }
+
+
+/* bufslurp • removes a given number of bytes from the head of the array */
+void
+bufslurp(struct buf *buf, size_t len) {
+ if (!buf || !buf->unit || len <= 0) return;
+ if (len >= buf->size) {
+ buf->size = 0;
+ return; }
+ buf->size -= len;
+ memmove(buf->data, buf->data + len, buf->size); }
+
+
+/* buftoi • converts the numbers at the beginning of the buf into an int */
+int
+buftoi(struct buf *buf, size_t offset_i, size_t *offset_o) {
+ int r = 0, neg = 0;
+ size_t i = offset_i;
+ if (!buf || !buf->size) return 0;
+ if (buf->data[i] == '+') i += 1;
+ else if (buf->data[i] == '-') {
+ neg = 1;
+ i += 1; }
+ while (i < buf->size && buf->data[i] >= '0' && buf->data[i] <= '9') {
+ r = (r * 10) + buf->data[i] - '0';
+ i += 1; }
+ if (offset_o) *offset_o = i;
+ return neg ? -r : r; }
+
+
+
+/* vbufprintf • stdarg variant of formatted printing into a buffer */
+void
+vbufprintf(struct buf *buf, const char *fmt, va_list ap) {
+ int n;
+ va_list ap_save;
+ if (buf == 0
+ || (buf->size >= buf->asize && !bufgrow (buf, buf->size + 1)))
+ return;
+
+ va_copy(ap_save, ap);
+ n = vsnprintf(buf->data + buf->size, buf->asize - buf->size, fmt, ap);
+
+ if (n < 0 || (size_t)n >= buf->asize - buf->size) {
+ size_t new_size = (n > 0) ? n : buf->size;
+ if (!bufgrow (buf, buf->size + new_size + 1))
+ return;
+
+ n = vsnprintf(buf->data + buf->size, buf->asize - buf->size, fmt, ap_save);
+ }
+ va_end(ap_save);
+
+ if (n < 0)
+ return;
+
+ buf->size += n;
+}
+
+/* vim: set filetype=c: */
154 ext/rinku/buffer.h
@@ -0,0 +1,154 @@
+/* buffer.h - automatic buffer structure */
+
+/*
+ * Copyright (c) 2008, Natacha Porté
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LITHIUM_BUFFER_H
+#define LITHIUM_BUFFER_H
+
+#include <stddef.h>
+
+#if defined(_MSC_VER)
+#define __attribute__(x)
+#define inline
+#define strncasecmp _strnicmp
+#define snprintf _snprintf
+#define va_copy(d,s) ((d) = (s))
+#endif
+
+/********************
+ * TYPE DEFINITIONS *
+ ********************/
+
+/* struct buf • character array buffer */
+struct buf {
+ char * data; /* actual character data */
+ size_t size; /* size of the string */
+ size_t asize; /* allocated size (0 = volatile buffer) */
+ size_t unit; /* reallocation unit size (0 = read-only buffer) */
+ int ref; }; /* reference count */
+
+/**********
+ * MACROS *
+ **********/
+
+#define STRLEN(x) (sizeof(x) - 1)
+
+/* CONST_BUF • global buffer from a string litteral */
+#define CONST_BUF(name, string) \
+ static struct buf name = { string, sizeof string -1, sizeof string }
+
+
+/* VOLATILE_BUF • macro for creating a volatile buffer on the stack */
+#define VOLATILE_BUF(name, strname) \
+ struct buf name = { strname, strlen(strname) }
+
+
+/* BUFPUTSL • optimized bufputs of a string litteral */
+#define BUFPUTSL(output, litteral) \
+ bufput(output, litteral, sizeof litteral - 1)
+
+
+
+/********************
+ * BUFFER FUNCTIONS *
+ ********************/
+
+/* bufcasecmp • case-insensitive buffer comparison */
+int
+bufcasecmp(const struct buf *, const struct buf *);
+
+/* bufcmp • case-sensitive buffer comparison */
+int
+bufcmp(const struct buf *, const struct buf *);
+
+/* bufcmps • case-sensitive comparison of a string to a buffer */
+int
+bufcmps(const struct buf *, const char *);
+
+/* bufprefix * compare the beginning of a buffer with a string */
+int
+bufprefix(const struct buf *buf, const char *prefix);
+
+/* bufdup • buffer duplication */
+struct buf *
+bufdup(const struct buf *, size_t)
+ __attribute__ ((malloc));
+
+/* bufgrow • increasing the allocated size to the given value */
+int
+bufgrow(struct buf *, size_t);
+
+/* bufnew • allocation of a new buffer */
+struct buf *
+bufnew(size_t)
+ __attribute__ ((malloc));
+
+/* bufnullterm • NUL-termination of the string array (making a C-string) */
+void
+bufnullterm(struct buf *);
+
+/* bufprintf • formatted printing to a buffer */
+void
+bufprintf(struct buf *, const char *, ...)
+ __attribute__ ((format (printf, 2, 3)));
+
+/* bufput • appends raw data to a buffer */
+void
+bufput(struct buf *, const void*, size_t);
+
+/* bufputs • appends a NUL-terminated string to a buffer */
+void
+bufputs(struct buf *, const char*);
+
+/* bufputc • appends a single char to a buffer */
+void
+bufputc(struct buf *, char);
+
+/* bufrelease • decrease the reference count and free the buffer if needed */
+void
+bufrelease(struct buf *);
+
+/* bufreset • frees internal data of the buffer */
+void
+bufreset(struct buf *);
+
+/* bufset • safely assigns a buffer to another */
+void
+bufset(struct buf **, struct buf *);
+
+/* bufslurp • removes a given number of bytes from the head of the array */
+void
+bufslurp(struct buf *, size_t);
+
+/* buftoi • converts the numbers at the beginning of the buf into an int */
+int
+buftoi(struct buf *, size_t, size_t *);
+
+
+
+#ifdef BUFFER_STDARG
+#include <stdarg.h>
+
+/* vbufprintf • stdarg variant of formatted printing into a buffer */
+void
+vbufprintf(struct buf *, const char*, va_list);
+
+#endif /* def BUFFER_STDARG */
+
+#endif /* ndef LITHIUM_BUFFER_H */
+
+/* vim: set filetype=c: */
4 ext/rinku/extconf.rb
@@ -0,0 +1,4 @@
+require 'mkmf'
+
+dir_config('rinku')
+create_makefile('rinku')
221 ext/rinku/html_autolink.c
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "autolink.h"
+#include "buffer.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+static void
+autolink_escape_cb(struct buf *ob, const struct buf *text, void *unused)
+{
+ size_t i = 0, org;
+
+ while (i < text->size) {
+ org = i;
+
+ while (i < text->size &&
+ text->data[i] != '<' &&
+ text->data[i] != '>' &&
+ text->data[i] != '&' &&
+ text->data[i] != '"')
+ i++;
+
+ if (i > org)
+ bufput(ob, text->data + org, i - org);
+
+ if (i >= text->size)
+ break;
+
+ switch (text->data[i]) {
+ case '<': BUFPUTSL(ob, "&lt;"); break;
+ case '>': BUFPUTSL(ob, "&gt;"); break;
+ case '&': BUFPUTSL(ob, "&amp;"); break;
+ case '"': BUFPUTSL(ob, "&quot;"); break;
+ default: bufputc(ob, text->data[i]); break;
+ }
+
+ i++;
+ }
+}
+
+static inline int
+is_closing_a(const char *tag, size_t size)
+{
+ size_t i;
+
+ if (tag[0] != '<' || size < STRLEN("</a>") || tag[1] != '/')
+ return 0;
+
+ i = 2;
+
+ while (i < size && isspace(tag[i]))
+ i++;
+
+ if (i == size || tag[i] != 'a')
+ return 0;
+
+ i++;
+
+ while (i < size && isspace(tag[i]))
+ i++;
+
+ if (i == size || tag[i] != '>')
+ return 0;
+
+ return i;
+}
+
+static size_t
+skip_tags(struct buf *ob, const char *text, size_t size)
+{
+ size_t i = 0;
+
+ while (i < size && text[i] != '>')
+ i++;
+
+ if (size > 3 && text[1] == 'a' && isspace(text[2])) {
+ while (i < size) {
+ size_t tag_len = is_closing_a(text + i, size - i);
+ if (tag_len) {
+ i += tag_len;
+ break;
+ }
+ i++;
+ }
+ }
+
+ bufput(ob, text, i + 1);
+ return i + 1;
+}
+
+void
+upshtml_autolink(
+ struct buf *ob,
+ struct buf *text,
+ unsigned int flags,
+ const char *link_attr,
+ void (*link_text_cb)(struct buf *ob, const struct buf *link, void *payload),
+ void *payload)
+{
+ size_t i, end;
+ struct buf *link = bufnew(16);
+ const char *active_chars;
+
+ if (!text || text->size == 0)
+ return;
+
+ switch (flags) {
+ case AUTOLINK_EMAILS:
+ active_chars = "<@";
+ break;
+
+ case AUTOLINK_URLS:
+ active_chars = "<w:";
+
+ case AUTOLINK_ALL:
+ active_chars = "<@w:";
+ break;
+
+ default:
+ return;
+ }
+
+ if (link_text_cb == NULL)
+ link_text_cb = &autolink_escape_cb;
+
+ bufgrow(ob, text->size);
+
+ i = end = 0;
+
+ while (i < text->size) {
+ size_t rewind;
+
+ while (end < text->size && strchr(active_chars, text->data[end]) == NULL)
+ end++;
+
+ bufput(ob, text->data + i, end - i);
+
+ if (end >= text->size)
+ break;
+
+ i = end;
+ link->size = 0;
+
+ switch (text->data[i]) {
+ case '@':
+ end = ups_autolink__email(&rewind, link, text->data + i, i, text->size - i);
+ if (end > 0) {
+ ob->size -= rewind;
+ BUFPUTSL(ob, "<a");
+ if (link_attr) bufputs(ob, link_attr);
+ BUFPUTSL(ob, " href=\"mailto:");
+ bufput(ob, link->data, link->size);
+ BUFPUTSL(ob, "\">");
+ link_text_cb(ob, link, payload);
+ BUFPUTSL(ob, "</a>");
+ }
+ break;
+
+ case 'w':
+ end = ups_autolink__www(&rewind, link, text->data + i, i, text->size - i);
+ if (end > 0) {
+ BUFPUTSL(ob, "<a");
+ if (link_attr) bufputs(ob, link_attr);
+ BUFPUTSL(ob, " href=\"http://");
+ bufput(ob, link->data, link->size);
+ BUFPUTSL(ob, "\">");
+ link_text_cb(ob, link, payload);
+ BUFPUTSL(ob, "</a>");
+ }
+ break;
+
+ case ':':
+ end = ups_autolink__url(&rewind, link, text->data + i, i, text->size - i);
+ if (end > 0) {
+ ob->size -= rewind;
+ BUFPUTSL(ob, "<a");
+ if (link_attr) bufputs(ob, link_attr);
+ BUFPUTSL(ob, " href=\"");
+ bufput(ob, link->data, link->size);
+ BUFPUTSL(ob, "\">");
+ link_text_cb(ob, link, payload);
+ BUFPUTSL(ob, "</a>");
+ }
+ break;
+
+ case '<':
+ end = skip_tags(ob, text->data + i, text->size - i);
+ break;
+
+ default:
+ end = 0;
+ break;
+ }
+
+ if (!end)
+ end = i + 1;
+ else {
+ i += end;
+ end = i;
+ }
+ }
+}
+
+
86 ext/rinku/rinku.c
@@ -0,0 +1,86 @@
+#include <stdio.h>
+#include "ruby.h"
+
+#include "autolink.h"
+#include "buffer.h"
+
+static VALUE rb_cRinku;
+
+extern void
+upshtml_autolink(
+ struct buf *ob,
+ struct buf *text,
+ unsigned int flags,
+ const char *link_attr,
+ void (*link_text_cb)(struct buf *ob, const struct buf *link, void *payload),
+ void *payload);
+
+static void
+autolink_callback(struct buf *link_text, const struct buf *link, void *block)
+{
+ VALUE rb_link, rb_link_text;
+ rb_link = rb_str_new(link->data, link->size);
+ rb_link_text = rb_funcall((VALUE)block, rb_intern("call"), 1, rb_link);
+ Check_Type(rb_link_text, T_STRING);
+ bufput(link_text, RSTRING_PTR(rb_link_text), RSTRING_LEN(rb_link_text));
+}
+
+static VALUE
+rb_rinku_autolink(int argc, VALUE *argv, VALUE self)
+{
+ VALUE result, rb_text, rb_mode, rb_html, rb_block;
+ struct buf input_buf = {0, 0, 0, 0, 0}, *output_buf;
+ int link_mode;
+ const char *link_attr = NULL;
+ ID mode_sym;
+
+ rb_scan_args(argc, argv, "3&", &rb_text, &rb_mode, &rb_html, &rb_block);
+
+ Check_Type(rb_text, T_STRING);
+ Check_Type(rb_mode, T_SYMBOL);
+
+ if (!NIL_P(rb_html)) {
+ Check_Type(rb_html, T_STRING);
+ link_attr = RSTRING_PTR(rb_html);
+ }
+
+ input_buf.data = RSTRING_PTR(rb_text);
+ input_buf.size = RSTRING_LEN(rb_text);
+ output_buf = bufnew(128);
+
+ mode_sym = SYM2ID(rb_mode);
+
+ if (mode_sym == rb_intern("all"))
+ link_mode = AUTOLINK_ALL;
+ else if (mode_sym == rb_intern("email_addresses"))
+ link_mode = AUTOLINK_EMAILS;
+ else if (mode_sym == rb_intern("urls"))
+ link_mode = AUTOLINK_URLS;
+ else
+ rb_raise(rb_eTypeError,
+ "Invalid linking mode (possible values are :all, :urls, :email_addresses)");
+
+ if (RTEST(rb_block))
+ upshtml_autolink(output_buf, &input_buf, AUTOLINK_ALL, link_attr, &autolink_callback, (void*)rb_block);
+ else
+ upshtml_autolink(output_buf, &input_buf, AUTOLINK_ALL, link_attr, NULL, NULL);
+
+ result = rb_str_new(output_buf->data, output_buf->size);
+ bufrelease(output_buf);
+
+ /* force the input encoding */
+ if (rb_respond_to(rb_text, rb_intern("encoding"))) {
+ VALUE encoding = rb_funcall(rb_text, rb_intern("encoding"), 0);
+ rb_funcall(result, rb_intern("force_encoding"), 1, encoding);
+ }
+
+ return result;
+}
+
+void Init_rinku()
+{
+ rb_cRinku = rb_define_class("Rinku", rb_cObject);
+ rb_define_singleton_method(rb_cRinku, "_auto_link", rb_rinku_autolink, -1);
+}
+
+
41 lib/rinku.rb
@@ -0,0 +1,41 @@
+require 'set'
+require 'cgi'
+
+class Rinku
+ def self.auto_link(text, *args, &block)
+ return '' if text.strip.empty?
+
+ options = args.size == 2 ? {} : (args.last.is_a?(::Hash) ? args.pop : {})
+ unless args.empty?
+ options[:link] = args[0] || :all
+ options[:html] = args[1] || {}
+ end
+
+ _auto_link(text, options[:link] || :all, tag_options(options[:html] || {}), &block)
+ end
+
+ private
+ BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
+ autoplay controls loop selected hidden scoped async
+ defer reversed ismap seemless muted required
+ autofocus novalidate formnovalidate open).to_set
+
+ def self.tag_options(options, escape = true)
+ unless options.empty?
+ attrs = []
+ options.each_pair do |key, value|
+ key = key.to_s
+ if BOOLEAN_ATTRIBUTES.include?(key)
+ attrs << %(#{key}="#{key}") if value
+ elsif !value.nil?
+ final_value = value.is_a?(Array) ? value.join(" ") : value
+ final_value = CGI.escapeHTML(final_value) if escape
+ attrs << %(#{key}="#{final_value}")
+ end
+ end
+ " #{attrs.sort * ' '}" unless attrs.empty?
+ end
+ end
+end
+
+require 'rinku.so'
34 rinku.gemspec
@@ -0,0 +1,34 @@
+Gem::Specification.new do |s|
+ s.name = 'rinku'
+ s.version = File.open('VERSION').read.strip
+ s.summary = "Mostly autolinking"
+ s.description = <<-EOF
+ A fast and very smart autolinking library that
+ acts as a drop-in replacement for Rails `auto_link`
+ EOF
+ s.email = 'vicent@github.com'
+ s.homepage = 'http://github.com/tanoku/rinku'
+ s.authors = ["Vicent Martí"]
+ # = MANIFEST =
+ s.files = %w[
+ COPYING
+ VERSION
+ README.markdown
+ Rakefile
+ ext/rinku/rinku.c
+ ext/rinku/autolink.c
+ ext/rinku/autolink.h
+ ext/rinku/buffer.c
+ ext/rinku/buffer.h
+ ext/rinku/html_autolink.c
+ ext/rinku/extconf.rb
+ lib/rinku.rb
+ rinku.gemspec
+ test/autolink_test.rb
+ ]
+ # = MANIFEST =
+ s.test_files = ["test/autolink_test.rb"]
+ s.extra_rdoc_files = ["COPYING"]
+ s.extensions = ["ext/rinku/extconf.rb"]
+ s.require_paths = ["lib"]
+end
135 test/autolink_test.rb
@@ -0,0 +1,135 @@
+# encoding: utf-8
+rootdir = File.dirname(File.dirname(__FILE__))
+$LOAD_PATH.unshift "#{rootdir}/lib"
+
+require 'test/unit'
+require 'cgi'
+require 'rinku'
+
+class RedcarpetAutolinkTest < Test::Unit::TestCase
+ def assert_linked(expected, url)
+ assert_equal expected, Rinku.auto_link(url)
+ end
+
+ def test_block
+ link = Rinku.auto_link("Find ur favorite pokeman @ http://www.pokemon.com") do |url|
+ assert_equal url, "http://www.pokemon.com"
+ "POKEMAN WEBSITE"
+ end
+
+ assert_equal link, "Find ur favorite pokeman @ <a href=\"http://www.pokemon.com\">POKEMAN WEBSITE</a>"
+ end
+
+ def test_link_attributes
+ assert_equal Rinku.auto_link("http://www.bash.org", :html => {:target => "_blank"}),
+ "<a target=\"_blank\" href=\"http://www.bash.org\">http://www.bash.org</a>"
+ end
+
+ def test_autolink_works
+ url = "http://example.com/"
+ assert_linked "<a href=\"#{url}\">#{url}</a>", url
+ end
+
+ def test_not_autolink_www
+ assert_linked "Awww... man", "Awww... man"
+ end
+
+ def test_does_not_terminate_on_dash
+ url = "http://example.com/Notification_Center-GitHub-20101108-140050.jpg"
+ assert_linked "<a href=\"#{url}\">#{url}</a>", url
+ end
+
+ def test_does_not_include_trailing_gt
+ url = "http://example.com"
+ assert_linked "&lt;<a href=\"#{url}\">#{url}</a>&gt;", "&lt;#{url}&gt;"
+ end
+
+ def test_links_with_anchors
+ url = "https://github.com/github/hubot/blob/master/scripts/cream.js#L20-20"
+ assert_linked "<a href=\"#{url}\">#{url}</a>", url
+ end
+
+ def test_links_like_rails
+ urls = %w(http://www.rubyonrails.com
+ http://www.rubyonrails.com:80
+ http://www.rubyonrails.com/~minam
+ https://www.rubyonrails.com/~minam
+ http://www.rubyonrails.com/~minam/url%20with%20spaces
+ http://www.rubyonrails.com/foo.cgi?something=here
+ http://www.rubyonrails.com/foo.cgi?something=here&and=here
+ http://www.rubyonrails.com/contact;new
+ http://www.rubyonrails.com/contact;new%20with%20spaces
+ http://www.rubyonrails.com/contact;new?with=query&string=params
+ http://www.rubyonrails.com/~minam/contact;new?with=query&string=params
+ http://en.wikipedia.org/wiki/Wikipedia:Today%27s_featured_picture_%28animation%29/January_20%2C_2007
+ http://www.mail-archive.com/rails@lists.rubyonrails.org/
+ http://www.amazon.com/Testing-Equal-Sign-In-Path/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1198861734&sr=8-1
+ http://en.wikipedia.org/wiki/Sprite_(computer_graphics)
+ http://en.wikipedia.org/wiki/Texas_hold'em
+ https://www.google.com/doku.php?id=gps:resource:scs:start
+ )
+
+ urls.each do |url|
+ assert_linked %(<a href="#{url}">#{CGI.escapeHTML(url)}</a>), url
+ end
+ end
+
+ def test_links_like_autolink_rails
+ email_raw = 'david@loudthinking.com'
+ email_result = %{<a href="mailto:#{email_raw}">#{email_raw}</a>}
+ email2_raw = '+david@loudthinking.com'
+ email2_result = %{<a href="mailto:#{email2_raw}">#{email2_raw}</a>}
+ link_raw = 'http://www.rubyonrails.com'
+ link_result = %{<a href="#{link_raw}">#{link_raw}</a>}
+ link_result_with_options = %{<a href="#{link_raw}" target="_blank">#{link_raw}</a>}
+ link2_raw = 'www.rubyonrails.com'
+ link2_result = %{<a href="http://#{link2_raw}">#{link2_raw}</a>}
+ link3_raw = 'http://manuals.ruby-on-rails.com/read/chapter.need_a-period/103#page281'
+ link3_result = %{<a href="#{link3_raw}">#{link3_raw}</a>}
+ link4_raw = 'http://foo.example.com/controller/action?parm=value&p2=v2#anchor123'
+ link4_result = %{<a href="#{link4_raw}">#{CGI.escapeHTML(link4_raw)}</a>}
+ link5_raw = 'http://foo.example.com:3000/controller/action'
+ link5_result = %{<a href="#{link5_raw}">#{link5_raw}</a>}
+ link6_raw = 'http://foo.example.com:3000/controller/action+pack'
+ link6_result = %{<a href="#{link6_raw}">#{link6_raw}</a>}
+ link7_raw = 'http://foo.example.com/controller/action?parm=value&p2=v2#anchor-123'
+ link7_result = %{<a href="#{link7_raw}">#{CGI.escapeHTML(link7_raw)}</a>}
+ link8_raw = 'http://foo.example.com:3000/controller/action.html'
+ link8_result = %{<a href="#{link8_raw}">#{link8_raw}</a>}
+ link9_raw = 'http://business.timesonline.co.uk/article/0,,9065-2473189,00.html'
+ link9_result = %{<a href="#{link9_raw}">#{link9_raw}</a>}
+ link10_raw = 'http://www.mail-archive.com/ruby-talk@ruby-lang.org/'
+ link10_result = %{<a href="#{link10_raw}">#{link10_raw}</a>}
+
+ assert_linked %(Go to #{link_result} and say hello to #{email_result}), "Go to #{link_raw} and say hello to #{email_raw}"
+ assert_linked %(<p>Link #{link_result}</p>), "<p>Link #{link_raw}</p>"
+ assert_linked %(<p>#{link_result} Link</p>), "<p>#{link_raw} Link</p>"
+ assert_linked %(Go to #{link_result}.), %(Go to #{link_raw}.)
+ assert_linked %(<p>Go to #{link_result}, then say hello to #{email_result}.</p>), %(<p>Go to #{link_raw}, then say hello to #{email_raw}.</p>)
+ assert_linked %(<p>Link #{link2_result}</p>), "<p>Link #{link2_raw}</p>"
+ assert_linked %(<p>#{link2_result} Link</p>), "<p>#{link2_raw} Link</p>"
+ assert_linked %(Go to #{link2_result}.), %(Go to #{link2_raw}.)
+ assert_linked %(<p>Say hello to #{email_result}, then go to #{link2_result},</p>), %(<p>Say hello to #{email_raw}, then go to #{link2_raw},</p>)
+ assert_linked %(<p>Link #{link3_result}</p>), "<p>Link #{link3_raw}</p>"
+ assert_linked %(<p>#{link3_result} Link</p>), "<p>#{link3_raw} Link</p>"
+ assert_linked %(Go to #{link3_result}.), %(Go to #{link3_raw}.)
+ assert_linked %(<p>Go to #{link3_result}. seriously, #{link3_result}? i think I'll say hello to #{email_result}. instead.</p>), %(<p>Go to #{link3_raw}. seriously, #{link3_raw}? i think I'll say hello to #{email_raw}. instead.</p>)
+ assert_linked %(<p>Link #{link4_result}</p>), "<p>Link #{link4_raw}</p>"
+ assert_linked %(<p>#{link4_result} Link</p>), "<p>#{link4_raw} Link</p>"
+ assert_linked %(<p>#{link5_result} Link</p>), "<p>#{link5_raw} Link</p>"
+ assert_linked %(<p>#{link6_result} Link</p>), "<p>#{link6_raw} Link</p>"
+ assert_linked %(<p>#{link7_result} Link</p>), "<p>#{link7_raw} Link</p>"
+ assert_linked %(<p>Link #{link8_result}</p>), "<p>Link #{link8_raw}</p>"
+ assert_linked %(<p>#{link8_result} Link</p>), "<p>#{link8_raw} Link</p>"
+ assert_linked %(Go to #{link8_result}.), %(Go to #{link8_raw}.)
+ assert_linked %(<p>Go to #{link8_result}. seriously, #{link8_result}? i think I'll say hello to #{email_result}. instead.</p>), %(<p>Go to #{link8_raw}. seriously, #{link8_raw}? i think I'll say hello to #{email_raw}. instead.</p>)
+ assert_linked %(<p>Link #{link9_result}</p>), "<p>Link #{link9_raw}</p>"
+ assert_linked %(<p>#{link9_result} Link</p>), "<p>#{link9_raw} Link</p>"
+ assert_linked %(Go to #{link9_result}.), %(Go to #{link9_raw}.)
+ assert_linked %(<p>Go to #{link9_result}. seriously, #{link9_result}? i think I'll say hello to #{email_result}. instead.</p>), %(<p>Go to #{link9_raw}. seriously, #{link9_raw}? i think I'll say hello to #{email_raw}. instead.</p>)
+ assert_linked %(<p>#{link10_result} Link</p>), "<p>#{link10_raw} Link</p>"
+ assert_linked email2_result, email2_raw
+ assert_linked "#{link_result} #{link_result} #{link_result}", "#{link_raw} #{link_raw} #{link_raw}"
+ assert_linked '<a href="http://www.rubyonrails.com">Ruby On Rails</a>', '<a href="http://www.rubyonrails.com">Ruby On Rails</a>'
+ end
+end
1  upskirt
@@ -0,0 +1 @@
+Subproject commit b3957282ce99d3484727840c7c074359b1ce06c5
Please sign in to comment.
Something went wrong with that request. Please try again.