From f2c2097b2c4aa7ee088df0d9a8568d5462fb3416 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Mon, 30 Jan 2012 23:36:57 -0600 Subject: [PATCH 01/22] limited github events to most recent 10 push events --- data.coffee | 2 +- models/gitevent.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data.coffee b/data.coffee index f167e07..8869ade 100644 --- a/data.coffee +++ b/data.coffee @@ -16,7 +16,7 @@ fetchEvents = cache.for tenMinutes, (cb) -> Event.load cb fetchGitEvents = cache.for tenMinutes, (cb) -> - GitEvent.load cb + GitEvent.loadPushEvents 10, cb module.exports = { load: (keys...) -> diff --git a/models/gitevent.coffee b/models/gitevent.coffee index 994b97a..27679f0 100644 --- a/models/gitevent.coffee +++ b/models/gitevent.coffee @@ -12,13 +12,13 @@ GitEvent = (data) -> this.repo = data.repo.name return -GitEvent.load = (cb) -> +GitEvent.loadPushEvents = (limit, cb) -> rest.get(eventFeed).on('complete', (data) -> filtered = data.filter (x) -> x.type == "PushEvent" - gitEvents = for x in filtered + gitEvents = for x in filtered[0...limit] new GitEvent x cb gitEvents From 157ea3ae5025258eb63980741dafd9f2d8955e9b Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Tue, 31 Jan 2012 00:03:35 -0600 Subject: [PATCH 02/22] added map to snow and company. Added link to github profile. --- public/css/app.styl | 61 ++++++++++++++++++++++++--------------------- views/layout.jade | 39 ++++++++++++++++------------- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/public/css/app.styl b/public/css/app.styl index 52d5ce7..5755868 100644 --- a/public/css/app.styl +++ b/public/css/app.styl @@ -92,8 +92,9 @@ form.join color #8CC84B font-size 1em line-height 1.2em + a + color #8CC84B a - display block clear both color #D2D8BA @@ -103,35 +104,37 @@ form.join color #9DD95C .time color #999 - - img - float left - width 48px - height 48px - margin 5px 10px 5px 0 - background #333 - border none - border-radius(4px) - &.more + .more + a text-align center - .subtitle - color #888 - font-size .7em - margin-top -3px - .time - color #888 - font-style italic - float right - font-size .6em - line-height 2.3em - - p - border none - font-size .75em - line-height 1.2em - margin-bottom 15px - word-wrap break-word - overflow hidden + display block + img + float left + width 48px + height 48px + margin 5px 10px 5px 0 + background #333 + border none + border-radius(4px) + + .subtitle + color #888 + font-size .7em + margin-top -3px + .time + color #888 + font-style italic + float right + font-size .6em + line-height 2.3em + + p + border none + font-size .75em + line-height 1.2em + margin-bottom 15px + word-wrap break-word + overflow hidden @media only screen and (max-width: 1000px) .section diff --git a/views/layout.jade b/views/layout.jade index 539f556..e28f387 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -33,23 +33,28 @@ html(lang="en") h2 events ul.container li - a(href='https://www.google.com/calendar/b/0/embed?src=nodekc.org_e8lg6hesldeld1utui23ebpg7k@group.calendar.google.com&ctz=America/Chicago&gsessionid=RKXEjz6D5m8kzZ0lZunOpQ') - .time Tue, Jan 31 7:00pm CST - h3 EVENT: Inaugural meetup - p We'll discuss the intent of the group and announce the first project. - + .time Tue, Jan 31 7:00pm CST + h3 EVENT: Inaugural meetup + .subtitle snow & company - + a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map + p We'll discuss the intent of the group and announce the first project. + li.more + a(href='https://www.google.com/calendar/b/0/embed?src=nodekc.org_e8lg6hesldeld1utui23ebpg7k@group.calendar.google.com&ctz=America/Chicago&gsessionid=RKXEjz6D5m8kzZ0lZunOpQ', target='_blank') calendar… h2 git ul.container each item in gitEvents li - a(href="http://github.com/" + item.actor, target="_blank") - img(src="https://secure.gravatar.com/avatar/" + item.actor_gravatar_id + "?s=140") - .time= item.timeago - h3= item.actor - .subtitle= item.repo - each commit in item.commits - p= commit.message - + .time= item.timeago + h3 + a(href='http://github.com/' + item.actor, target='_blank') + img(src='https://secure.gravatar.com/avatar/' + item.actor_gravatar_id + '?s=140') + #{item.actor} + .subtitle= item.repo + each commit in item.commits + p= commit.message + li.more + a(href='https://github.com/nodekc') nodekc on github… + .messages.section h2 messages ul.container @@ -60,8 +65,8 @@ html(lang="en") h3= item.subject .subtitle by #{item.author} p= item.body - li - a.more(href='https://groups.google.com/group/nodekc') google group… + li.more + a(href='https://groups.google.com/group/nodekc') google group… .tweets.section h2 tweets ul.container @@ -72,5 +77,5 @@ html(lang="en") .time #{item.timeago} h3= item.created_by p= item.tweet - li - a.more(href='https://twitter.com/#!/nodekc') more tweets… + li.more + a(href='https://twitter.com/#!/nodekc') more tweets… From 9faf07b6766ad6bebc26b9c89de1a21888f68740 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Tue, 31 Jan 2012 00:22:56 -0600 Subject: [PATCH 03/22] Fixed broken form action to google group signup after group rename. --- views/layout.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/layout.jade b/views/layout.jade index e28f387..4848998 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -24,7 +24,7 @@ html(lang="en") h1 NodeKC - form(action='http://groups.google.com/group/kc-nodejs/boxsubscribe').join + form(action='http://groups.google.com/group/nodekc/boxsubscribe').join input(type='submit', value='join') label input(type='email', name='email', placeholder='your@email.com') From e10e6bdeaa04ca951f1d388ce3d28bbe221fb83c Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Tue, 31 Jan 2012 11:28:34 -0600 Subject: [PATCH 04/22] fixed issue where tweets would start nesting because of no clear on the li --- public/css/app.styl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/css/app.styl b/public/css/app.styl index 5755868..43c5dd0 100644 --- a/public/css/app.styl +++ b/public/css/app.styl @@ -81,6 +81,9 @@ form.join box-shadow(1px 1px 1px 1px rgba(0, 0, 0, .5)) border-radius(4px) background #222 + + li + clear both h2 margin-top 20px text-align center From 3c63e966fdc9ed07c356d6ace7f86b476ae58fc5 Mon Sep 17 00:00:00 2001 From: Scott Smerchek Date: Thu, 2 Feb 2012 08:32:15 -0600 Subject: [PATCH 05/22] Added twitter-text library to linkify tweets --- models/gitevent.coffee | 1 - models/tweet.coffee | 6 ++++-- views/layout.jade | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/models/gitevent.coffee b/models/gitevent.coffee index 27679f0..2b59fdc 100644 --- a/models/gitevent.coffee +++ b/models/gitevent.coffee @@ -4,7 +4,6 @@ moment = require 'moment' eventFeed = 'https://api.github.com/orgs/nodekc/events' GitEvent = (data) -> - console.log data this.actor = data.actor.login this.actor_gravatar_id = data.actor.gravatar_id this.timeago = moment(new Date(data.created_at)).fromNow() diff --git a/models/tweet.coffee b/models/tweet.coffee index da1a533..2a35ce5 100644 --- a/models/tweet.coffee +++ b/models/tweet.coffee @@ -1,10 +1,12 @@ moment = require 'moment' rest = require 'restler' -twitterFeed = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5' +twitter = require 'twitter-text' +twitterFeed = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5&include_entities=1' Tweet = (data) -> + console.log data this.created_by = data.from_user - this.tweet = data.text + this.tweet = twitter.autoLink data.text, urlEntities: data.entities.urls this.timeago = moment(new Date(data.created_at)).fromNow() this.created_at = data.created_at return diff --git a/views/layout.jade b/views/layout.jade index 4848998..f539fb2 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -72,10 +72,10 @@ html(lang="en") ul.container each item in tweets li + img(src='http://api.twitter.com/1/users/profile_image/' + item.created_by + '.png', alt=item.created_by) + .time #{item.timeago} a(href='http://twitter.com/' + item.created_by, target='_blank') - img(src='http://api.twitter.com/1/users/profile_image/' + item.created_by + '.png', alt=item.created_by) - .time #{item.timeago} h3= item.created_by - p= item.tweet + p!= item.tweet li.more a(href='https://twitter.com/#!/nodekc') more tweets… From c4e07a54c5e47bc802e1336efcae6b4e7e321eba Mon Sep 17 00:00:00 2001 From: Scott Smerchek Date: Thu, 2 Feb 2012 21:13:19 -0600 Subject: [PATCH 06/22] Included modified twitter-text library and changed tweet.coffee to require it instead. (Should switch back to twitter-text with npm when nodejs issue is fixed) --- lib/twitter-text.js | 919 ++++++++++++++++++++++++++++++++++++++++++++ models/tweet.coffee | 2 +- 2 files changed, 920 insertions(+), 1 deletion(-) create mode 100644 lib/twitter-text.js diff --git a/lib/twitter-text.js b/lib/twitter-text.js new file mode 100644 index 0000000..575893c --- /dev/null +++ b/lib/twitter-text.js @@ -0,0 +1,919 @@ +if (typeof window === "undefined" || window === null) { + window = { twttr: {} }; +} +if (window.twttr == null) { + window.twttr = {}; +} +if (typeof twttr === "undefined" || twttr === null) { + twttr = {}; +} + +(function() { + twttr.txt = {}; + twttr.txt.regexen = {}; + + var HTML_ENTITIES = { + '&': '&', + '>': '>', + '<': '<', + '"': '"', + "'": ''' + }; + + // HTML escaping + twttr.txt.htmlEscape = function(text) { + return text && text.replace(/[&"'><]/g, function(character) { + return HTML_ENTITIES[character]; + }); + }; + + // Builds a RegExp + function regexSupplant(regex, flags) { + flags = flags || ""; + if (typeof regex !== "string") { + if (regex.global && flags.indexOf("g") < 0) { + flags += "g"; + } + if (regex.ignoreCase && flags.indexOf("i") < 0) { + flags += "i"; + } + if (regex.multiline && flags.indexOf("m") < 0) { + flags += "m"; + } + + regex = regex.source; + } + + return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { + var newRegex = twttr.txt.regexen[name] || ""; + if (typeof newRegex !== "string") { + newRegex = newRegex.source; + } + return newRegex; + }), flags); + } + + // simple string interpolation + function stringSupplant(str, values) { + return str.replace(/#\{(\w+)\}/g, function(match, name) { + return values[name] || ""; + }); + } + + function addCharsToCharClass(charClass, start, end) { + var s = String.fromCharCode(start); + if (end !== start) { + s += "-" + String.fromCharCode(end); + } + charClass.push(s); + return charClass; + } + + // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand + // to access both the list of characters and a pattern suitible for use with String#split + // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE + var fromCode = String.fromCharCode; + var UNICODE_SPACES = [ + fromCode(0x0020), // White_Space # Zs SPACE + fromCode(0x0085), // White_Space # Cc + fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE + fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK + fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR + fromCode(0x2028), // White_Space # Zl LINE SEPARATOR + fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR + fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE + fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE + fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE + ]; + addCharsToCharClass(UNICODE_SPACES, 0x009, 0x00D); // White_Space # Cc [5] .. + addCharsToCharClass(UNICODE_SPACES, 0x2000, 0x200A); // White_Space # Zs [11] EN QUAD..HAIR SPACE + + var INVALID_CHARS = [ + fromCode(0xFFFE), + fromCode(0xFEFF), // BOM + fromCode(0xFFFF) // Special + ]; + addCharsToCharClass(INVALID_CHARS, 0x202A, 0x202E); // Directional change + + twttr.txt.regexen.spaces_group = regexSupplant(UNICODE_SPACES.join("")); + twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]"); + twttr.txt.regexen.invalid_chars_group = regexSupplant(INVALID_CHARS.join("")); + twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; + twttr.txt.regexen.atSigns = /[@@]/; + twttr.txt.regexen.extractMentions = regexSupplant(/(^|[^a-zA-Z0-9_])(#{atSigns})([a-zA-Z0-9_]{1,20})/g); + twttr.txt.regexen.extractReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); + twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; + twttr.txt.regexen.extractMentionsOrLists = regexSupplant(/(^|[^a-zA-Z0-9_])(#{atSigns})([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g); + + var nonLatinHashtagChars = []; + // Cyrillic + addCharsToCharClass(nonLatinHashtagChars, 0x0400, 0x04ff); // Cyrillic + addCharsToCharClass(nonLatinHashtagChars, 0x0500, 0x0527); // Cyrillic Supplement + addCharsToCharClass(nonLatinHashtagChars, 0x2de0, 0x2dff); // Cyrillic Extended A + addCharsToCharClass(nonLatinHashtagChars, 0xa640, 0xa69f); // Cyrillic Extended B + // Hangul (Korean) + addCharsToCharClass(nonLatinHashtagChars, 0x1100, 0x11ff); // Hangul Jamo + addCharsToCharClass(nonLatinHashtagChars, 0x3130, 0x3185); // Hangul Compatibility Jamo + addCharsToCharClass(nonLatinHashtagChars, 0xA960, 0xA97F); // Hangul Jamo Extended-A + addCharsToCharClass(nonLatinHashtagChars, 0xAC00, 0xD7AF); // Hangul Syllables + addCharsToCharClass(nonLatinHashtagChars, 0xD7B0, 0xD7FF); // Hangul Jamo Extended-B + addCharsToCharClass(nonLatinHashtagChars, 0xFFA1, 0xFFDC); // half-width Hangul + // Japanese and Chinese + addCharsToCharClass(nonLatinHashtagChars, 0x30A1, 0x30FA); // Katakana (full-width) + addCharsToCharClass(nonLatinHashtagChars, 0x30FC, 0x30FE); // Katakana Chouon and iteration marks (full-width) + addCharsToCharClass(nonLatinHashtagChars, 0xFF66, 0xFF9F); // Katakana (half-width) + addCharsToCharClass(nonLatinHashtagChars, 0xFF70, 0xFF70); // Katakana Chouon (half-width) + addCharsToCharClass(nonLatinHashtagChars, 0xFF10, 0xFF19); // \ + addCharsToCharClass(nonLatinHashtagChars, 0xFF21, 0xFF3A); // - Latin (full-width) + addCharsToCharClass(nonLatinHashtagChars, 0xFF41, 0xFF5A); // / + addCharsToCharClass(nonLatinHashtagChars, 0x3041, 0x3096); // Hiragana + addCharsToCharClass(nonLatinHashtagChars, 0x3099, 0x309E); // Hiragana voicing and iteration mark + addCharsToCharClass(nonLatinHashtagChars, 0x3400, 0x4DBF); // Kanji (CJK Extension A) + addCharsToCharClass(nonLatinHashtagChars, 0x4E00, 0x9FFF); // Kanji (Unified) + // -- Disabled as it breaks the Regex. + //addCharsToCharClass(nonLatinHashtagChars, 0x20000, 0x2A6DF); // Kanji (CJK Extension B) + addCharsToCharClass(nonLatinHashtagChars, 0x2A700, 0x2B73F); // Kanji (CJK Extension C) + addCharsToCharClass(nonLatinHashtagChars, 0x2B740, 0x2B81F); // Kanji (CJK Extension D) + addCharsToCharClass(nonLatinHashtagChars, 0x2F800, 0x2FA1F); // Kanji (CJK supplement) + addCharsToCharClass(nonLatinHashtagChars, 0x3005, 0x3005); // Kanji iteration mark + addCharsToCharClass(nonLatinHashtagChars, 0x303B, 0x303B); // Han iteration mark + + twttr.txt.regexen.nonLatinHashtagChars = regexSupplant(nonLatinHashtagChars.join("")); + // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") + twttr.txt.regexen.latinAccentChars = regexSupplant("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþş\\303\\277"); + + twttr.txt.regexen.endScreenNameMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/); + + // A hashtag must contain characters, numbers and underscores, but not all numbers. + twttr.txt.regexen.hashtagAlpha = regexSupplant(/[a-z_#{latinAccentChars}#{nonLatinHashtagChars}]/i); + twttr.txt.regexen.hashtagAlphaNumeric = regexSupplant(/[a-z0-9_#{latinAccentChars}#{nonLatinHashtagChars}]/i); + twttr.txt.regexen.endHashtagMatch = /^(?:[##]|:\/\/)/; + twttr.txt.regexen.hashtagBoundary = regexSupplant(/(?:^|$|[^&\/a-z0-9_#{latinAccentChars}#{nonLatinHashtagChars}])/); + twttr.txt.regexen.autoLinkHashtags = regexSupplant(/(#{hashtagBoundary})(#|#)(#{hashtagAlphaNumeric}*#{hashtagAlpha}#{hashtagAlphaNumeric}*)/gi); + twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g; + twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; + + // URL related hash regex collection + twttr.txt.regexen.validPrecedingChars = regexSupplant(/(?:[^-\/"'!=A-Za-z0-9_@@##\.#{invalid_chars_group}]|^)/); + + twttr.txt.regexen.invalidDomainChars = stringSupplant("#{punct}#{spaces_group}#{invalid_chars_group}", twttr.txt.regexen); + twttr.txt.regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/); + twttr.txt.regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/); + twttr.txt.regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/); + twttr.txt.regexen.validGTLD = regexSupplant(/(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx)(?=[^a-zA-Z]|$))/); + twttr.txt.regexen.validCCTLD = regexSupplant(/(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^a-zA-Z]|$))/); + twttr.txt.regexen.validPunycode = regexSupplant(/(?:xn--[0-9a-z]+)/); + twttr.txt.regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/); + twttr.txt.regexen.validAsciiDomain = regexSupplant(/(?:(?:[a-z0-9#{latinAccentChars}]+)\.)+(?:#{validGTLD}|#{validCCTLD}|#{validPunycode})/gi); + twttr.txt.regexen.invalidShortDomain = regexSupplant(/^#{validDomainName}#{validCCTLD}$/); + + twttr.txt.regexen.validPortNumber = regexSupplant(/[0-9]+/); + + twttr.txt.regexen.validGeneralUrlPathChars = regexSupplant(/[a-z0-9!\*';:=\+,\.\$\/%#\[\]\-_~|&#{latinAccentChars}]/i); + // Allow URL paths to contain balanced parens + // 1. Used in Wikipedia URLs like /Primer_(film) + // 2. Used in IIS sessions like /S(dfd346)/ + twttr.txt.regexen.validUrlBalancedParens = regexSupplant(/\(#{validGeneralUrlPathChars}+\)/i); + // Valid end-of-path chracters (so /foo. does not gobble the period). + // 1. Allow =&# for empty URL parameters and other URL-join artifacts + twttr.txt.regexen.validUrlPathEndingChars = regexSupplant(/[\+\-a-z0-9=_#\/#{latinAccentChars}]|(?:#{validUrlBalancedParens})/i); + // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/ + twttr.txt.regexen.validUrlPath = regexSupplant('(?:' + + '(?:' + + '#{validGeneralUrlPathChars}*' + + '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' + + '#{validUrlPathEndingChars}'+ + ')|(?:@#{validGeneralUrlPathChars}+\/)'+ + ')', 'i'); + + twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i; + twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i; + twttr.txt.regexen.extractUrl = regexSupplant( + '(' + // $1 total match + '(#{validPrecedingChars})' + // $2 Preceeding chracter + '(' + // $3 URL + '(https?:\\/\\/)?' + // $4 Protocol (optional) + '(#{validDomain})' + // $5 Domain(s) + '(?::(#{validPortNumber}))?' + // $6 Port number (optional) + '(\\/#{validUrlPath}*)?' + // $7 URL Path + '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $8 Query String + ')' + + ')' + , 'gi'); + + twttr.txt.regexen.validTcoUrl = /^https?:\/\/t\.co\/[a-z0-9]+/i; + + // These URL validation pattern strings are based on the ABNF from RFC 3986 + twttr.txt.regexen.validateUrlUnreserved = /[a-z0-9\-._~]/i; + twttr.txt.regexen.validateUrlPctEncoded = /(?:%[0-9a-f]{2})/i; + twttr.txt.regexen.validateUrlSubDelims = /[!$&'()*+,;=]/i; + twttr.txt.regexen.validateUrlPchar = regexSupplant('(?:' + + '#{validateUrlUnreserved}|' + + '#{validateUrlPctEncoded}|' + + '#{validateUrlSubDelims}|' + + '[:|@]' + + ')', 'i'); + + twttr.txt.regexen.validateUrlScheme = /(?:[a-z][a-z0-9+\-.]*)/i; + twttr.txt.regexen.validateUrlUserinfo = regexSupplant('(?:' + + '#{validateUrlUnreserved}|' + + '#{validateUrlPctEncoded}|' + + '#{validateUrlSubDelims}|' + + ':' + + ')*', 'i'); + + twttr.txt.regexen.validateUrlDecOctet = /(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9]{2})|(?:2[0-4][0-9])|(?:25[0-5]))/i; + twttr.txt.regexen.validateUrlIpv4 = regexSupplant(/(?:#{validateUrlDecOctet}(?:\.#{validateUrlDecOctet}){3})/i); + + // Punting on real IPv6 validation for now + twttr.txt.regexen.validateUrlIpv6 = /(?:\[[a-f0-9:\.]+\])/i; + + // Also punting on IPvFuture for now + twttr.txt.regexen.validateUrlIp = regexSupplant('(?:' + + '#{validateUrlIpv4}|' + + '#{validateUrlIpv6}' + + ')', 'i'); + + // This is more strict than the rfc specifies + twttr.txt.regexen.validateUrlSubDomainSegment = /(?:[a-z0-9](?:[a-z0-9_\-]*[a-z0-9])?)/i; + twttr.txt.regexen.validateUrlDomainSegment = /(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?)/i; + twttr.txt.regexen.validateUrlDomainTld = /(?:[a-z](?:[a-z0-9\-]*[a-z0-9])?)/i; + twttr.txt.regexen.validateUrlDomain = regexSupplant(/(?:(?:#{validateUrlSubDomainSegment]}\.)*(?:#{validateUrlDomainSegment]}\.)#{validateUrlDomainTld})/i); + + twttr.txt.regexen.validateUrlHost = regexSupplant('(?:' + + '#{validateUrlIp}|' + + '#{validateUrlDomain}' + + ')', 'i'); + + // Unencoded internationalized domains - this doesn't check for invalid UTF-8 sequences + twttr.txt.regexen.validateUrlUnicodeSubDomainSegment = /(?:(?:[a-z0-9]|[^\u0000-\u007f])(?:(?:[a-z0-9_\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i; + twttr.txt.regexen.validateUrlUnicodeDomainSegment = /(?:(?:[a-z0-9]|[^\u0000-\u007f])(?:(?:[a-z0-9\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i; + twttr.txt.regexen.validateUrlUnicodeDomainTld = /(?:(?:[a-z]|[^\u0000-\u007f])(?:(?:[a-z0-9\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i; + twttr.txt.regexen.validateUrlUnicodeDomain = regexSupplant(/(?:(?:#{validateUrlUnicodeSubDomainSegment}\.)*(?:#{validateUrlUnicodeDomainSegment}\.)#{validateUrlUnicodeDomainTld})/i); + + twttr.txt.regexen.validateUrlUnicodeHost = regexSupplant('(?:' + + '#{validateUrlIp}|' + + '#{validateUrlUnicodeDomain}' + + ')', 'i'); + + twttr.txt.regexen.validateUrlPort = /[0-9]{1,5}/; + + twttr.txt.regexen.validateUrlUnicodeAuthority = regexSupplant( + '(?:(#{validateUrlUserinfo})@)?' + // $1 userinfo + '(#{validateUrlUnicodeHost})' + // $2 host + '(?::(#{validateUrlPort}))?' //$3 port + , "i"); + + twttr.txt.regexen.validateUrlAuthority = regexSupplant( + '(?:(#{validateUrlUserinfo})@)?' + // $1 userinfo + '(#{validateUrlHost})' + // $2 host + '(?::(#{validateUrlPort}))?' // $3 port + , "i"); + + twttr.txt.regexen.validateUrlPath = regexSupplant(/(\/#{validateUrlPchar}*)*/i); + twttr.txt.regexen.validateUrlQuery = regexSupplant(/(#{validateUrlPchar}|\/|\?)*/i); + twttr.txt.regexen.validateUrlFragment = regexSupplant(/(#{validateUrlPchar}|\/|\?)*/i); + + // Modified version of RFC 3986 Appendix B + twttr.txt.regexen.validateUrlUnencoded = regexSupplant( + '^' + // Full URL + '(?:' + + '([^:/?#]+):\\/\\/' + // $1 Scheme + ')?' + + '([^/?#]*)' + // $2 Authority + '([^?#]*)' + // $3 Path + '(?:' + + '\\?([^#]*)' + // $4 Query + ')?' + + '(?:' + + '#(.*)' + // $5 Fragment + ')?$' + , "i"); + + + // Default CSS class for auto-linked URLs + var DEFAULT_URL_CLASS = "tweet-url"; + // Default CSS class for auto-linked lists (along with the url class) + var DEFAULT_LIST_CLASS = "list-slug"; + // Default CSS class for auto-linked usernames (along with the url class) + var DEFAULT_USERNAME_CLASS = "username"; + // Default CSS class for auto-linked hashtags (along with the url class) + var DEFAULT_HASHTAG_CLASS = "hashtag"; + // HTML attribute for robot nofollow behavior (default) + var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; + + // Simple object cloning function for simple objects + function clone(o) { + var r = {}; + for (var k in o) { + if (o.hasOwnProperty(k)) { + r[k] = o[k]; + } + } + + return r; + } + + twttr.txt.autoLink = function(text, options) { + options = clone(options || {}); + return twttr.txt.autoLinkUsernamesOrLists( + twttr.txt.autoLinkUrlsCustom( + twttr.txt.autoLinkHashtags(text, options), + options), + options); + }; + + + twttr.txt.autoLinkUsernamesOrLists = function(text, options) { + options = clone(options || {}); + + options.urlClass = options.urlClass || DEFAULT_URL_CLASS; + options.listClass = options.listClass || DEFAULT_LIST_CLASS; + options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; + options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; + options.listUrlBase = options.listUrlBase || "http://twitter.com/"; + if (!options.suppressNoFollow) { + var extraHtml = HTML_ATTR_NO_FOLLOW; + } + + var newText = "", + splitText = twttr.txt.splitTags(text); + + for (var index = 0; index < splitText.length; index++) { + var chunk = splitText[index]; + + if (index !== 0) { + newText += ((index % 2 === 0) ? ">" : "<"); + } + + if (index % 4 !== 0) { + newText += chunk; + } else { + newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) { + var after = chunk.slice(offset + match.length); + + var d = { + before: before, + at: at, + user: twttr.txt.htmlEscape(user), + slashListname: twttr.txt.htmlEscape(slashListname), + extraHtml: extraHtml, + preChunk: "", + chunk: twttr.txt.htmlEscape(chunk), + postChunk: "" + }; + for (var k in options) { + if (options.hasOwnProperty(k)) { + d[k] = options[k]; + } + } + + if (slashListname && !options.suppressLists) { + // the link is a list + var list = d.chunk = stringSupplant("#{user}#{slashListname}", d); + d.list = twttr.txt.htmlEscape(list.toLowerCase()); + return stringSupplant("#{before}#{at}#{preChunk}#{chunk}#{postChunk}", d); + } else { + if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { + // Followed by something that means we don't autolink + return match; + } else { + // this is a screen name + d.chunk = twttr.txt.htmlEscape(user); + d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : ""; + return stringSupplant("#{before}#{at}#{preChunk}#{chunk}#{postChunk}", d); + } + } + }); + } + } + + return newText; + }; + + twttr.txt.autoLinkHashtags = function(text, options) { + options = clone(options || {}); + options.urlClass = options.urlClass || DEFAULT_URL_CLASS; + options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; + options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; + if (!options.suppressNoFollow) { + var extraHtml = HTML_ATTR_NO_FOLLOW; + } + + return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text, offset, chunk) { + var after = chunk.slice(offset + match.length); + if (after.match(twttr.txt.regexen.endHashtagMatch)) + return match; + + var d = { + before: before, + hash: twttr.txt.htmlEscape(hash), + preText: "", + text: twttr.txt.htmlEscape(text), + postText: "", + extraHtml: extraHtml + }; + + for (var k in options) { + if (options.hasOwnProperty(k)) { + d[k] = options[k]; + } + } + + return stringSupplant("#{before}#{hash}#{preText}#{text}#{postText}", d); + }); + }; + + + twttr.txt.autoLinkUrlsCustom = function(text, options) { + options = clone(options || {}); + if (!options.suppressNoFollow) { + options.rel = "nofollow"; + } + if (options.urlClass) { + options["class"] = options.urlClass; + delete options.urlClass; + } + + // remap url entities to hash + var urlEntities, i, len; + if(options.urlEntities) { + urlEntities = {}; + for(i = 0, len = options.urlEntities.length; i < len; i++) { + urlEntities[options.urlEntities[i].url] = options.urlEntities[i]; + } + } + + delete options.suppressNoFollow; + delete options.suppressDataScreenName; + delete options.listClass; + delete options.usernameClass; + delete options.usernameUrlBase; + delete options.listUrlBase; + + return text.replace(twttr.txt.regexen.extractUrl, function(match, all, before, url, protocol, port, domain, path, queryString) { + var tldComponents; + + if (protocol) { + var htmlAttrs = ""; + var after = ""; + for (var k in options) { + htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); + } + + // In the case of t.co URLs, don't allow additional path characters. + if (url.match(twttr.txt.regexen.validTcoUrl)) { + url = RegExp.lastMatch; + after = RegExp.rightContext; + } + + var d = { + before: before, + htmlAttrs: htmlAttrs, + url: twttr.txt.htmlEscape(url), + after: after + }; + if (urlEntities && urlEntities[url] && urlEntities[url].display_url) { + d.displayUrl = twttr.txt.htmlEscape(urlEntities[url].display_url); + } else { + d.displayUrl = d.url; + } + + return stringSupplant("#{before}#{displayUrl}#{after}", d); + } else { + return all; + } + }); + }; + + twttr.txt.extractMentions = function(text) { + var screenNamesOnly = [], + screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); + + for (var i = 0; i < screenNamesWithIndices.length; i++) { + var screenName = screenNamesWithIndices[i].screenName; + screenNamesOnly.push(screenName); + } + + return screenNamesOnly; + }; + + twttr.txt.extractMentionsWithIndices = function(text) { + if (!text) { + return []; + } + + var possibleScreenNames = [], + position = 0; + + text.replace(twttr.txt.regexen.extractMentions, function(match, before, atSign, screenName, offset, chunk) { + var after = chunk.slice(offset + match.length); + if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { + var startPosition = text.indexOf(atSign + screenName, position); + position = startPosition + screenName.length + 1; + possibleScreenNames.push({ + screenName: screenName, + indices: [startPosition, position] + }); + } + }); + + return possibleScreenNames; + }; + + /** + * Extract list or user mentions. + * (Presence of listSlug indicates a list) + */ + twttr.txt.extractMentionsOrListsWithIndices = function(text) { + if (!text) { + return []; + } + + var possibleNames = [], + position = 0; + + text.replace(twttr.txt.regexen.extractMentionsOrLists, function(match, before, atSign, screenName, slashListname, offset, chunk) { + var after = chunk.slice(offset + match.length); + if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { + slashListname = slashListname || ''; + var startPosition = text.indexOf(atSign + screenName + slashListname, position); + position = startPosition + screenName.length + slashListname.length + 1; + possibleNames.push({ + screenName: screenName, + listSlug: slashListname, + indices: [startPosition, position] + }); + } + }); + + return possibleNames; + }; + + + twttr.txt.extractReplies = function(text) { + if (!text) { + return null; + } + + var possibleScreenName = text.match(twttr.txt.regexen.extractReply); + if (!possibleScreenName || + RegExp.rightContext.match(twttr.txt.regexen.endScreenNameMatch)) { + return null; + } + + return possibleScreenName[1]; + }; + + twttr.txt.extractUrls = function(text) { + var urlsOnly = [], + urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); + + for (var i = 0; i < urlsWithIndices.length; i++) { + urlsOnly.push(urlsWithIndices[i].url); + } + + return urlsOnly; + }; + + twttr.txt.extractUrlsWithIndices = function(text) { + if (!text) { + return []; + } + + var urls = [], + position = 0; + + text.replace(twttr.txt.regexen.extractUrl, function(match, all, before, url, protocol, domain, port, path, query) { + var startPosition = text.indexOf(url, position), + endPosition = startPosition + url.length; + + // if protocol is missing and domain contains non-ASCII characters, + // extract ASCII-only domains. + if (!protocol) { + var lastUrl = null, + lastUrlInvalidMatch = false, + asciiEndPosition = 0; + domain.replace(twttr.txt.regexen.validAsciiDomain, function(asciiDomain) { + var asciiStartPosition = domain.indexOf(asciiDomain, asciiEndPosition); + asciiEndPosition = asciiStartPosition + asciiDomain.length + lastUrl = { + url: asciiDomain, + indices: [startPosition + asciiStartPosition, startPosition + asciiEndPosition] + } + lastUrlInvalidMatch = asciiDomain.match(twttr.txt.regexen.invalidShortDomain); + if (!lastUrlInvalidMatch) { + urls.push(lastUrl); + } + }); + + // no ASCII-only domain found. Skip the entire URL. + if (lastUrl == null) { + return; + } + + // lastUrl only contains domain. Need to add path and query if they exist. + if (path) { + if (lastUrlInvalidMatch) { + urls.push(lastUrl); + } + lastUrl.url = url.replace(domain, lastUrl.url); + lastUrl.indices[1] = endPosition; + } + } else { + // In the case of t.co URLs, don't allow additional path characters. + if (url.match(twttr.txt.regexen.validTcoUrl)) { + url = RegExp.lastMatch; + endPosition = startPosition + url.length; + } + urls.push({ + url: url, + indices: [startPosition, endPosition] + }); + } + }); + + return urls; + }; + + twttr.txt.extractHashtags = function(text) { + var hashtagsOnly = [], + hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); + + for (var i = 0; i < hashtagsWithIndices.length; i++) { + hashtagsOnly.push(hashtagsWithIndices[i].hashtag); + } + + return hashtagsOnly; + }; + + twttr.txt.extractHashtagsWithIndices = function(text) { + if (!text) { + return []; + } + + var tags = [], + position = 0; + + text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText, offset, chunk) { + var after = chunk.slice(offset + match.length); + if (after.match(twttr.txt.regexen.endHashtagMatch)) + return; + var startPosition = text.indexOf(hash + hashText, position); + position = startPosition + hashText.length + 1; + tags.push({ + hashtag: hashText, + indices: [startPosition, position] + }); + }); + + return tags; + }; + + // this essentially does text.split(/<|>/) + // except that won't work in IE, where empty strings are ommitted + // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others + // but "<<".split("<") => ["", "", ""] + twttr.txt.splitTags = function(text) { + var firstSplits = text.split("<"), + secondSplits, + allSplits = [], + split; + + for (var i = 0; i < firstSplits.length; i += 1) { + split = firstSplits[i]; + if (!split) { + allSplits.push(""); + } else { + secondSplits = split.split(">"); + for (var j = 0; j < secondSplits.length; j += 1) { + allSplits.push(secondSplits[j]); + } + } + } + + return allSplits; + }; + + twttr.txt.hitHighlight = function(text, hits, options) { + var defaultHighlightTag = "em"; + + hits = hits || []; + options = options || {}; + + if (hits.length === 0) { + return text; + } + + var tagName = options.tag || defaultHighlightTag, + tags = ["<" + tagName + ">", ""], + chunks = twttr.txt.splitTags(text), + split, + i, + j, + result = "", + chunkIndex = 0, + chunk = chunks[0], + prevChunksLen = 0, + chunkCursor = 0, + startInChunk = false, + chunkChars = chunk, + flatHits = [], + index, + hit, + tag, + placed, + hitSpot; + + for (i = 0; i < hits.length; i += 1) { + for (j = 0; j < hits[i].length; j += 1) { + flatHits.push(hits[i][j]); + } + } + + for (index = 0; index < flatHits.length; index += 1) { + hit = flatHits[index]; + tag = tags[index % 2]; + placed = false; + + while (chunk != null && hit >= prevChunksLen + chunk.length) { + result += chunkChars.slice(chunkCursor); + if (startInChunk && hit === prevChunksLen + chunkChars.length) { + result += tag; + placed = true; + } + + if (chunks[chunkIndex + 1]) { + result += "<" + chunks[chunkIndex + 1] + ">"; + } + + prevChunksLen += chunkChars.length; + chunkCursor = 0; + chunkIndex += 2; + chunk = chunks[chunkIndex]; + chunkChars = chunk; + startInChunk = false; + } + + if (!placed && chunk != null) { + hitSpot = hit - prevChunksLen; + result += chunkChars.slice(chunkCursor, hitSpot) + tag; + chunkCursor = hitSpot; + if (index % 2 === 0) { + startInChunk = true; + } else { + startInChunk = false; + } + } else if(!placed) { + placed = true; + result += tag; + } + } + + if (chunk != null) { + if (chunkCursor < chunkChars.length) { + result += chunkChars.slice(chunkCursor); + } + for (index = chunkIndex + 1; index < chunks.length; index += 1) { + result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); + } + } + + return result; + }; + + var MAX_LENGTH = 140; + + // Characters not allowed in Tweets + var INVALID_CHARACTERS = [ + // BOM + fromCode(0xFFFE), + fromCode(0xFEFF), + + // Special + fromCode(0xFFFF), + + // Directional Change + fromCode(0x202A), + fromCode(0x202B), + fromCode(0x202C), + fromCode(0x202D), + fromCode(0x202E) + ]; + + // Check the text for any reason that it may not be valid as a Tweet. This is meant as a pre-validation + // before posting to api.twitter.com. There are several server-side reasons for Tweets to fail but this pre-validation + // will allow quicker feedback. + // + // Returns false if this text is valid. Otherwise one of the following strings will be returned: + // + // "too_long": if the text is too long + // "empty": if the text is nil or empty + // "invalid_characters": if the text contains non-Unicode or any of the disallowed Unicode characters + twttr.txt.isInvalidTweet = function(text) { + if (!text) { + return "empty"; + } + + if (text.length > MAX_LENGTH) { + return "too_long"; + } + + for (var i = 0; i < INVALID_CHARACTERS.length; i++) { + if (text.indexOf(INVALID_CHARACTERS[i]) >= 0) { + return "invalid_characters"; + } + } + + return false; + }; + + twttr.txt.isValidTweetText = function(text) { + return !twttr.txt.isInvalidTweet(text); + }; + + twttr.txt.isValidUsername = function(username) { + if (!username) { + return false; + } + + var extracted = twttr.txt.extractMentions(username); + + // Should extract the username minus the @ sign, hence the .slice(1) + return extracted.length === 1 && extracted[0] === username.slice(1); + }; + + var VALID_LIST_RE = regexSupplant(/^#{autoLinkUsernamesOrLists}$/); + + twttr.txt.isValidList = function(usernameList) { + var match = usernameList.match(VALID_LIST_RE); + + // Must have matched and had nothing before or after + return !!(match && match[1] == "" && match[4]); + }; + + twttr.txt.isValidHashtag = function(hashtag) { + if (!hashtag) { + return false; + } + + var extracted = twttr.txt.extractHashtags(hashtag); + + // Should extract the hashtag minus the # sign, hence the .slice(1) + return extracted.length === 1 && extracted[0] === hashtag.slice(1); + }; + + twttr.txt.isValidUrl = function(url, unicodeDomains, requireProtocol) { + if (unicodeDomains == null) { + unicodeDomains = true; + } + + if (requireProtocol == null) { + requireProtocol = true; + } + + if (!url) { + return false; + } + + var urlParts = url.match(twttr.txt.regexen.validateUrlUnencoded); + + if (!urlParts || urlParts[0] !== url) { + return false; + } + + var scheme = urlParts[1], + authority = urlParts[2], + path = urlParts[3], + query = urlParts[4], + fragment = urlParts[5]; + + if (!( + (!requireProtocol || (isValidMatch(scheme, twttr.txt.regexen.validateUrlScheme) && scheme.match(/^https?$/i))) && + isValidMatch(path, twttr.txt.regexen.validateUrlPath) && + isValidMatch(query, twttr.txt.regexen.validateUrlQuery, true) && + isValidMatch(fragment, twttr.txt.regexen.validateUrlFragment, true) + )) { + return false; + } + + return (unicodeDomains && isValidMatch(authority, twttr.txt.regexen.validateUrlUnicodeAuthority)) || + (!unicodeDomains && isValidMatch(authority, twttr.txt.regexen.validateUrlAuthority)); + }; + + function isValidMatch(string, regex, optional) { + if (!optional) { + // RegExp["$&"] is the text of the last match + // blank strings are ok, but are falsy, so we check stringiness instead of truthiness + return ((typeof string === "string") && string.match(regex) && RegExp["$&"] === string); + } + + // RegExp["$&"] is the text of the last match + return (!string || (string.match(regex) && RegExp["$&"] === string)); + } + + if (typeof module != 'undefined' && module.exports) { + module.exports = twttr.txt; + } + +}()); diff --git a/models/tweet.coffee b/models/tweet.coffee index 2a35ce5..32c5a04 100644 --- a/models/tweet.coffee +++ b/models/tweet.coffee @@ -1,6 +1,6 @@ moment = require 'moment' rest = require 'restler' -twitter = require 'twitter-text' +twitter = require '../lib/twitter-text' twitterFeed = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5&include_entities=1' Tweet = (data) -> From 8675b26842d911e66f627e9128b4522ed46a858d Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Fri, 3 Feb 2012 19:20:55 -0600 Subject: [PATCH 07/22] removed dependency on building a c++ module for xml parsing --- app.coffee | 3 +-- cache.coffee | 14 +++++++------- data.coffee | 11 +++++------ models/event.coffee | 13 ++++--------- models/gitevent.coffee | 18 ++++++++---------- models/message.coffee | 41 ++++++++++++++++++++++------------------- models/tweet.coffee | 21 +++++++++++---------- package.json | 7 +++---- 8 files changed, 61 insertions(+), 67 deletions(-) diff --git a/app.coffee b/app.coffee index 375691a..ff6bf0c 100755 --- a/app.coffee +++ b/app.coffee @@ -1,5 +1,3 @@ -#!/usr/bin/env node - express = require 'express' app = express.createServer() port = process.env.PORT || 3000 @@ -23,6 +21,7 @@ app.set 'view engine', 'jade' app.get '/', data.load('tweets', 'messages', 'events', 'gitEvents'), (req, res) -> res.render 'layout', res.data + app.listen port console.log 'server listening on port ' + port diff --git a/cache.coffee b/cache.coffee index 22018c5..840ad28 100644 --- a/cache.coffee +++ b/cache.coffee @@ -1,13 +1,13 @@ module.exports = { for: (expiration, work) -> - lastResult = null - lastModified = null + last_result = null + last_modified = null return (complete) -> - if lastModified? and (new Date).getTime() - expiration < lastModified - complete lastResult + if last_modified? and (new Date).getTime() - expiration < last_modified + complete last_result else work (data) -> - lastResult = data - lastModified = (new Date).getTime() - complete lastResult + last_result = data + last_modified = (new Date).getTime() + complete last_result } \ No newline at end of file diff --git a/data.coffee b/data.coffee index 8869ade..5340290 100644 --- a/data.coffee +++ b/data.coffee @@ -4,18 +4,18 @@ Tweet = require './models/tweet' Message = require './models/message' GitEvent = require './models/gitevent' -tenMinutes = 10 * 60 * 1000 +ten_minutes = 10 * 60 * 1000 -fetchMessages = cache.for tenMinutes, (cb) -> +fetchMessages = cache.for ten_minutes, (cb) -> Message.load cb -fetchTweets = cache.for tenMinutes, (cb) -> +fetchTweets = cache.for ten_minutes, (cb) -> Tweet.load cb -fetchEvents = cache.for tenMinutes, (cb) -> +fetchEvents = cache.for ten_minutes, (cb) -> Event.load cb -fetchGitEvents = cache.for tenMinutes, (cb) -> +fetchGitEvents = cache.for ten_minutes, (cb) -> GitEvent.loadPushEvents 10, cb module.exports = { @@ -40,7 +40,6 @@ module.exports = { cb() gitEvents: (data, cb) -> fetchGitEvents (events) -> - console.log events data.gitEvents = events cb() } \ No newline at end of file diff --git a/models/event.coffee b/models/event.coffee index 9d1f98e..206c83d 100644 --- a/models/event.coffee +++ b/models/event.coffee @@ -1,16 +1,13 @@ moment = require 'moment' -parser = require 'xml2json' rest = require 'restler' ical = require 'ical' require 'datejs' -parseFeed = (feed) -> - JSON.parse(parser.toJson(feed)).feed.entry +event_feed = 'http://www.google.com/calendar/ical/nodekc.org_e8lg6hesldeld1utui23ebpg7k%40group.calendar.google.com/public/basic.ics' formatDate = (start, end) -> - start = moment(start.setTimezone("CST")) - end = moment(end.setTimezone("CST")) - + start = moment(start.setTimezone('CST')) + end = moment(end.setTimezone('CST')) date = start.format('ddd, MMM D') if end.diff(start, 'days') == 1 and start.hours() == 0 @@ -18,8 +15,6 @@ formatDate = (start, end) -> date + start.format(' h:mma CST') -eventFeed = 'http://www.google.com/calendar/ical/nodekc.org_e8lg6hesldeld1utui23ebpg7k%40group.calendar.google.com/public/basic.ics' - Event = (data) -> this.title = data.summary this.location = data.location @@ -29,7 +24,7 @@ Event = (data) -> return Event.load = (cb) -> - ical.fromURL eventFeed, {}, (err, calendar) -> + ical.fromURL event_feed, {}, (err, calendar) -> calendar or= {} events = for k,v of calendar new Event v diff --git a/models/gitevent.coffee b/models/gitevent.coffee index 27679f0..e8c2944 100644 --- a/models/gitevent.coffee +++ b/models/gitevent.coffee @@ -1,10 +1,9 @@ rest = require 'restler' moment = require 'moment' -eventFeed = 'https://api.github.com/orgs/nodekc/events' +event_feed_url = 'https://api.github.com/orgs/nodekc/events' GitEvent = (data) -> - console.log data this.actor = data.actor.login this.actor_gravatar_id = data.actor.gravatar_id this.timeago = moment(new Date(data.created_at)).fromNow() @@ -13,15 +12,14 @@ GitEvent = (data) -> return GitEvent.loadPushEvents = (limit, cb) -> - rest.get(eventFeed).on('complete', (data) -> - - filtered = data.filter (x) -> - x.type == "PushEvent" + rest.get(event_feed_url) + .on 'complete', (data) -> + filtered = data.filter (x) -> + x.type == "PushEvent" - gitEvents = for x in filtered[0...limit] - new GitEvent x + gitEvents = for x in filtered[0...limit] + new GitEvent x - cb gitEvents - ) + cb gitEvents module.exports = GitEvent \ No newline at end of file diff --git a/models/message.coffee b/models/message.coffee index 14bf901..5b60fc2 100644 --- a/models/message.coffee +++ b/models/message.coffee @@ -1,11 +1,8 @@ moment = require 'moment' -parser = require 'xml2json' rest = require 'restler' +FeedParser = require 'feedparser' -messageFeed = 'http://groups.google.com/group/nodekc/feed/atom_v1_0_topics.xml' - -parseFeed = (feed) -> - JSON.parse(parser.toJson(feed)).feed.entry +message_feed_url = 'http://groups.google.com/group/nodekc/feed/atom_v1_0_topics.xml' striphtml = (value) -> value.replace(/<(?:.|\n)*?>/gm, ' ') @@ -16,22 +13,28 @@ formatContent = (content) -> content Message = (data) -> - this.subject = data.title.$t - this.body = formatContent data.summary.$t - this.author = data.author.name - this.timeago = moment(new Date(data.updated)).fromNow() - this.url = data.link.href - this.author = data.author.name + this.subject = data.title + this.body = formatContent data.description + this.timeago = moment(new Date(data.date)).fromNow() + this.url = data.link + this.author = data.author return Message.load = (cb) -> - rest.get(messageFeed).on('complete', (data) -> - data or= '' - - messages = for x in parseFeed data - new Message x - - cb messages - ) + rest.get(message_feed_url) + .on 'complete', (data) -> + parser = new FeedParser() + + articles = [] + + parser.on 'article', (article) -> + articles.push(article) + + parser.parseString data + + messages = for x in articles + new Message x + + cb messages module.exports = Message \ No newline at end of file diff --git a/models/tweet.coffee b/models/tweet.coffee index da1a533..232bc87 100644 --- a/models/tweet.coffee +++ b/models/tweet.coffee @@ -1,6 +1,7 @@ moment = require 'moment' rest = require 'restler' -twitterFeed = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5' + +twitter_feed_url = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5' Tweet = (data) -> this.created_by = data.from_user @@ -10,14 +11,14 @@ Tweet = (data) -> return Tweet.load = (cb) -> - rest.get(twitterFeed).on('complete', (data) -> - data or= {} - data.results or= [] - - tweets = for x in data.results - new Tweet x - - cb tweets - ) + rest.get(twitter_feed_url) + .on 'complete', (data) -> + data or= {} + data.results or= [] + + tweets = for x in data.results + new Tweet x + + cb tweets module.exports = Tweet \ No newline at end of file diff --git a/package.json b/package.json index e2a7e27..175010c 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,17 @@ { - "name": "node-kc", + "name": "nodekc", "version": "0.0.1", "dependencies": { "express": "~2.5.6", "coffee-script": "latest", "jade": "~v0.20.0", "stylus": "~v0.22.5", - "easysax": "latest", "restler": "latest", "underscore.date": "latest", - "xml2json": "latest", "ical": "latest", "moment": "latest", - "datejs": "latest" + "datejs": "latest", + "feedparser": "0.9.1" } } From 7ba2f9e844cf2d12868a92433e7455df0284ab5f Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Fri, 3 Feb 2012 19:27:20 -0600 Subject: [PATCH 08/22] merged conflict with tweets --- models/tweet.coffee | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/models/tweet.coffee b/models/tweet.coffee index de57ea3..4eae654 100644 --- a/models/tweet.coffee +++ b/models/tweet.coffee @@ -1,15 +1,10 @@ moment = require 'moment' rest = require 'restler' -<<<<<<< HEAD +twitter = require '../lib/twitter-text' twitter_feed_url = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5' -======= -twitter = require '../lib/twitter-text' -twitterFeed = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5&include_entities=1' ->>>>>>> c4e07a54c5e47bc802e1336efcae6b4e7e321eba Tweet = (data) -> - console.log data this.created_by = data.from_user this.tweet = twitter.autoLink data.text, urlEntities: data.entities.urls this.timeago = moment(new Date(data.created_at)).fromNow() @@ -27,4 +22,4 @@ Tweet.load = (cb) -> cb tweets -module.exports = Tweet \ No newline at end of file +module.exports = Tweet From a57c5ee89040d6dda4773b95f90ebbfa220d3c28 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Fri, 3 Feb 2012 19:33:46 -0600 Subject: [PATCH 09/22] Fixed url to include entities missed in merge --- models/tweet.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/tweet.coffee b/models/tweet.coffee index 4eae654..62af2a6 100644 --- a/models/tweet.coffee +++ b/models/tweet.coffee @@ -2,10 +2,11 @@ moment = require 'moment' rest = require 'restler' twitter = require '../lib/twitter-text' -twitter_feed_url = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5' +twitter_feed_url = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5&include_entities=1' Tweet = (data) -> this.created_by = data.from_user + console.log data this.tweet = twitter.autoLink data.text, urlEntities: data.entities.urls this.timeago = moment(new Date(data.created_at)).fromNow() this.created_at = data.created_at From f06bd5de1e4512d4563a3807a9442b2f3daa7c0e Mon Sep 17 00:00:00 2001 From: Scott Smerchek Date: Fri, 3 Feb 2012 20:26:40 -0600 Subject: [PATCH 10/22] Urls in tweets will now be shown in the green, differintiating them from the tweet text. --- models/tweet.coffee | 3 +-- public/css/app.styl | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/models/tweet.coffee b/models/tweet.coffee index 62af2a6..f571dff 100644 --- a/models/tweet.coffee +++ b/models/tweet.coffee @@ -6,8 +6,7 @@ twitter_feed_url = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5&incl Tweet = (data) -> this.created_by = data.from_user - console.log data - this.tweet = twitter.autoLink data.text, urlEntities: data.entities.urls + this.tweet = twitter.autoLink data.text, { urlEntities: data.entities.urls, urlClass: 'tweet-url' } this.timeago = moment(new Date(data.created_at)).fromNow() this.created_at = data.created_at return diff --git a/public/css/app.styl b/public/css/app.styl index 43c5dd0..35be9d8 100644 --- a/public/css/app.styl +++ b/public/css/app.styl @@ -107,6 +107,10 @@ form.join color #9DD95C .time color #999 + + a.tweet-url + color #8CC84B + .more a text-align center From faada3212ac14b3a055aba8fc2eca3eea4900043 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Fri, 3 Feb 2012 21:50:58 -0600 Subject: [PATCH 11/22] indent git commits. closes #1 --- public/css/app.styl | 12 ++++++++++++ views/layout.jade | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/public/css/app.styl b/public/css/app.styl index 43c5dd0..ca153ec 100644 --- a/public/css/app.styl +++ b/public/css/app.styl @@ -84,6 +84,16 @@ form.join li clear both + ul.inset + margin-left 58px + li + font-size .75em + line-height 1.2em + word-wrap break-word + overflow hidden + clear none + margin-bottom 10px + h2 margin-top 20px text-align center @@ -95,6 +105,8 @@ form.join color #8CC84B font-size 1em line-height 1.2em + &.dim + color #777 a color #8CC84B a diff --git a/views/layout.jade b/views/layout.jade index f539fb2..f37f403 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -31,10 +31,17 @@ html(lang="en") .events.section h2 events - ul.container + ul.container + li + .time Wed, Feb 8 7:00pm CST + h3 EVENT: Makeup Meetup + .subtitle snow & company - + a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map + p For those who couldn't make the first meetup. We'll go over the principles of the group, discuss the project we're working on, and write some code. + li .time Tue, Jan 31 7:00pm CST - h3 EVENT: Inaugural meetup + h3.dim EVENT: Inaugural meetup .subtitle snow & company - a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map p We'll discuss the intent of the group and announce the first project. @@ -50,8 +57,9 @@ html(lang="en") img(src='https://secure.gravatar.com/avatar/' + item.actor_gravatar_id + '?s=140') #{item.actor} .subtitle= item.repo - each commit in item.commits - p= commit.message + ul.inset + each commit in item.commits + li= commit.message li.more a(href='https://github.com/nodekc') nodekc on github… From 7a0cbed25e5024c6e4d5dc36b277bc72a0c56661 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Fri, 3 Feb 2012 23:20:21 -0600 Subject: [PATCH 12/22] Updated readme to include information on github and twitter api's --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e7368b5..354bc5e 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -Please feel free to submit pull requests to improve the nodekc website. \ No newline at end of file +Please feel free to submit pull requests to improve the nodekc website. + + +#Twitter API Key + +visit http://developer.twitter.com and create an application. Once created, you'll be given an API key and SECRET. + +##Set up your environment variables + +export TWITTER_CONSUMER_KEY= +export TWITTER_CONSUMER_SECRET= + +#Github API Key + + + +#Set up your environment variables + +export GITHUB_CLIENT_ID= +export GITHUB_CLIENT_SECRET= From bdaa208c403456ee03550add1b3e0604e1ca48ac Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Sun, 12 Feb 2012 16:08:51 -0600 Subject: [PATCH 13/22] Updated events --- views/layout.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/layout.jade b/views/layout.jade index f37f403..a764ef5 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -34,7 +34,7 @@ html(lang="en") ul.container li .time Wed, Feb 8 7:00pm CST - h3 EVENT: Makeup Meetup + h3.dim EVENT: Makeup Meetup .subtitle snow & company - a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map p For those who couldn't make the first meetup. We'll go over the principles of the group, discuss the project we're working on, and write some code. From 6ba2d2a8ed890ce1f88eb4e27f1a5f994fbb3304 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Sun, 12 Feb 2012 16:10:55 -0600 Subject: [PATCH 14/22] Added link to meeting review --- views/layout.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/layout.jade b/views/layout.jade index a764ef5..8962f87 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -38,7 +38,7 @@ html(lang="en") .subtitle snow & company - a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map p For those who couldn't make the first meetup. We'll go over the principles of the group, discuss the project we're working on, and write some code. - + a(href='http://groups.google.com/group/nodekc/browse_thread/thread/6c7c7d9332dff7af') meeting review li .time Tue, Jan 31 7:00pm CST h3.dim EVENT: Inaugural meetup From 3f3c92a67d8d346c29be8cf4002b0032f59703b1 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Sun, 12 Feb 2012 16:15:54 -0600 Subject: [PATCH 15/22] styled review link --- public/css/app.styl | 6 +++++- views/layout.jade | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/css/app.styl b/public/css/app.styl index 8bfd674..6f77ce5 100644 --- a/public/css/app.styl +++ b/public/css/app.styl @@ -119,7 +119,11 @@ form.join color #9DD95C .time color #999 - + a.review-url + color #8CC84B + clear both + display block + margin-top 5px a.tweet-url color #8CC84B diff --git a/views/layout.jade b/views/layout.jade index 8962f87..e43d191 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -38,7 +38,7 @@ html(lang="en") .subtitle snow & company - a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map p For those who couldn't make the first meetup. We'll go over the principles of the group, discuss the project we're working on, and write some code. - a(href='http://groups.google.com/group/nodekc/browse_thread/thread/6c7c7d9332dff7af') meeting review + a.review-url(href='http://groups.google.com/group/nodekc/browse_thread/thread/6c7c7d9332dff7af') See Wednesday's meeting review li .time Tue, Jan 31 7:00pm CST h3.dim EVENT: Inaugural meetup From 6799d0a3ec63eb92e6a96ea7c91ec234924e5c0e Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Thu, 16 Feb 2012 19:48:23 -0600 Subject: [PATCH 16/22] added new event --- views/layout.jade | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/views/layout.jade b/views/layout.jade index e43d191..2992c3c 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -34,6 +34,13 @@ html(lang="en") ul.container li .time Wed, Feb 8 7:00pm CST + + h3 EVENT: Weekend Meetup + .subtitle snow & company - + a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map + p We'll work toward another user story. + .time Tue, Feb 19 1:00pm CST + h3.dim EVENT: Makeup Meetup .subtitle snow & company - a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map From e29d68a657b27d91907a323bf74a94aa2998ab3a Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Fri, 17 Feb 2012 11:16:18 -0600 Subject: [PATCH 17/22] accidentally switched around times. --- views/layout.jade | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/views/layout.jade b/views/layout.jade index 2992c3c..7486f8a 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -33,13 +33,14 @@ html(lang="en") h2 events ul.container li - .time Wed, Feb 8 7:00pm CST + .time Tue, Feb 19 1:00pm CST h3 EVENT: Weekend Meetup .subtitle snow & company - a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map p We'll work toward another user story. - .time Tue, Feb 19 1:00pm CST + li + .time Wed, Feb 8 7:00pm CST h3.dim EVENT: Makeup Meetup .subtitle snow & company - From 0b3037614f8a2e948299bc0bd54a507333e6d890 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Fri, 17 Feb 2012 11:21:43 -0600 Subject: [PATCH 18/22] Fix incorrect data --- views/layout.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/layout.jade b/views/layout.jade index 7486f8a..b8fd6de 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -33,7 +33,7 @@ html(lang="en") h2 events ul.container li - .time Tue, Feb 19 1:00pm CST + .time Sun, Feb 19 1:00pm CST h3 EVENT: Weekend Meetup .subtitle snow & company - From 344b401c98b0cfb90c8c880c73b1d9eaec7c267a Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Tue, 28 Feb 2012 18:51:28 -0600 Subject: [PATCH 19/22] Added date for next event --- views/layout.jade | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/views/layout.jade b/views/layout.jade index b8fd6de..9883086 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -31,7 +31,15 @@ html(lang="en") .events.section h2 events - ul.container + ul.container + li + .time Sun, Mar 11 1:30pm CST + + h3 EVENT: Weekend Meetup + .subtitle TBA + p We'll work on + a(href='http://github.com/nodekc/stalkqr') stalkqr + li .time Sun, Feb 19 1:00pm CST From 1604b3ab5f3453bb6c07223fae25ecf5ee71aca6 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Tue, 28 Feb 2012 18:54:11 -0600 Subject: [PATCH 20/22] Added versions --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 175010c..364c01c 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,15 @@ "version": "0.0.1", "dependencies": { "express": "~2.5.6", - "coffee-script": "latest", + "coffee-script": "v1.2.0", "jade": "~v0.20.0", "stylus": "~v0.22.5", - "restler": "latest", - "underscore.date": "latest", - "ical": "latest", - "moment": "latest", - "datejs": "latest", - "feedparser": "0.9.1" + "restler": "v0.2.5", + "underscore.date": "v0.6.1", + "ical": "v0.0.5", + "moment": "v1.4.0", + "datejs": "v0.0.2", + "feedparser": "v0.9.1" } } From 518f8eca818d9c2e8620ebe0fcb1488277b733b7 Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Sat, 10 Mar 2012 21:43:35 -0600 Subject: [PATCH 21/22] Updated location of next event --- views/layout.jade | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/layout.jade b/views/layout.jade index 9883086..1fcf515 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -36,7 +36,8 @@ html(lang="en") .time Sun, Mar 11 1:30pm CST h3 EVENT: Weekend Meetup - .subtitle TBA + .subtitle snow & company - + a(href='http://g.co/maps/s9ms6', target='_blank', title='Map to Snow & Company') map p We'll work on a(href='http://github.com/nodekc/stalkqr') stalkqr From f05a2c0e15a2ae18f8941608f1cf1714fc2f819e Mon Sep 17 00:00:00 2001 From: Joe Andaverde Date: Wed, 2 May 2012 19:35:06 -0500 Subject: [PATCH 22/22] Updated site for next meeting --- views/layout.jade | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/views/layout.jade b/views/layout.jade index 1fcf515..9d12443 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -32,6 +32,11 @@ html(lang="en") .events.section h2 events ul.container + li + .time Tues, May 15 6:30pm CST + h3 EVENT: Regular Meetup + .subtitle TBA + p Topic to be announced li .time Sun, Mar 11 1:30pm CST