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= 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 f167e07..5340290 100644 --- a/data.coffee +++ b/data.coffee @@ -4,19 +4,19 @@ 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) -> - GitEvent.load cb +fetchGitEvents = cache.for ten_minutes, (cb) -> + GitEvent.loadPushEvents 10, cb module.exports = { load: (keys...) -> @@ -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/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/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 994b97a..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() @@ -12,16 +11,15 @@ GitEvent = (data) -> this.repo = data.repo.name return -GitEvent.load = (cb) -> - rest.get(eventFeed).on('complete', (data) -> - - filtered = data.filter (x) -> - x.type == "PushEvent" +GitEvent.loadPushEvents = (limit, cb) -> + rest.get(event_feed_url) + .on 'complete', (data) -> + filtered = data.filter (x) -> + x.type == "PushEvent" - gitEvents = for x in filtered - 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..f571dff 100644 --- a/models/tweet.coffee +++ b/models/tweet.coffee @@ -1,23 +1,25 @@ moment = require 'moment' rest = require 'restler' -twitterFeed = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5' +twitter = require '../lib/twitter-text' + +twitter_feed_url = 'http://search.twitter.com/search.json?q=%40nodekc&rpp=5&include_entities=1' Tweet = (data) -> this.created_by = data.from_user - this.tweet = data.text + 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 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 +module.exports = Tweet diff --git a/package.json b/package.json index e2a7e27..364c01c 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", + "coffee-script": "v1.2.0", "jade": "~v0.20.0", "stylus": "~v0.22.5", - "easysax": "latest", - "restler": "latest", - "underscore.date": "latest", - "xml2json": "latest", - "ical": "latest", - "moment": "latest", - "datejs": "latest" + "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" } } diff --git a/public/css/app.styl b/public/css/app.styl index 52d5ce7..6f77ce5 100644 --- a/public/css/app.styl +++ b/public/css/app.styl @@ -81,6 +81,19 @@ form.join box-shadow(1px 1px 1px 1px rgba(0, 0, 0, .5)) border-radius(4px) background #222 + + 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 @@ -92,8 +105,11 @@ form.join color #8CC84B font-size 1em line-height 1.2em + &.dim + color #777 + a + color #8CC84B a - display block clear both color #D2D8BA @@ -103,35 +119,45 @@ form.join color #9DD95C .time color #999 + a.review-url + color #8CC84B + clear both + display block + margin-top 5px + a.tweet-url + color #8CC84B - 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..9d12443 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') @@ -33,23 +33,58 @@ 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 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 + + 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 on + a(href='http://github.com/nodekc/stalkqr') stalkqr + + li + .time Sun, 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. + li + .time Wed, Feb 8 7: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 + 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.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 + .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 + .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 + ul.inset each commit in item.commits - p= commit.message - + li= commit.message + li.more + a(href='https://github.com/nodekc') nodekc on github… + .messages.section h2 messages ul.container @@ -60,17 +95,17 @@ 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 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 - li - a.more(href='https://twitter.com/#!/nodekc') more tweets… + p!= item.tweet + li.more + a(href='https://twitter.com/#!/nodekc') more tweets…