From 42fce58a91db8a7d0906003c8061d6b1ff1d5295 Mon Sep 17 00:00:00 2001 From: Luke Hagan Date: Sun, 6 Nov 2011 18:31:28 -0800 Subject: [PATCH] initial commit --- .gitignore | 11 + BeautifulSoup.py | 2014 ++++++++++++++++++++++ LICENSE-bottle.txt | 19 + LICENSE.md | 22 + bottle.py | 2901 ++++++++++++++++++++++++++++++++ instapaper.py | 28 + login.tpl | 22 + newsblur.py | 340 ++++ newsblur_interface.py | 90 + serv.py | 64 + static/css/popover.css | 76 + static/css/style.css | 636 +++++++ static/fonts/OFL.txt | 97 ++ static/fonts/iconic_stroke.otf | Bin 0 -> 44880 bytes static/humans.txt | 43 + static/index.html | 82 + static/js/libs/zepto.js | 1412 ++++++++++++++++ static/js/libs/zepto.min.js | 4 + static/js/mylibs/.gitignore | 2 + static/js/plugins.js | 42 + static/js/script.js | 203 +++ static/robots.txt | 5 + 22 files changed, 8113 insertions(+) create mode 100644 .gitignore create mode 100644 BeautifulSoup.py create mode 100644 LICENSE-bottle.txt create mode 100644 LICENSE.md create mode 100755 bottle.py create mode 100644 instapaper.py create mode 100644 login.tpl create mode 100644 newsblur.py create mode 100644 newsblur_interface.py create mode 100644 serv.py create mode 100644 static/css/popover.css create mode 100755 static/css/style.css create mode 100644 static/fonts/OFL.txt create mode 100644 static/fonts/iconic_stroke.otf create mode 100755 static/humans.txt create mode 100755 static/index.html create mode 100644 static/js/libs/zepto.js create mode 100644 static/js/libs/zepto.min.js create mode 100755 static/js/mylibs/.gitignore create mode 100755 static/js/plugins.js create mode 100755 static/js/script.js create mode 100755 static/robots.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd7ef28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.pyc +.DS_Store +build +*.log +*.log.* +logs +.sass-cache +*~ +~* +*.pbxuser +*.mode1v3 \ No newline at end of file diff --git a/BeautifulSoup.py b/BeautifulSoup.py new file mode 100644 index 0000000..4b17b85 --- /dev/null +++ b/BeautifulSoup.py @@ -0,0 +1,2014 @@ +"""Beautiful Soup +Elixir and Tonic +"The Screen-Scraper's Friend" +http://www.crummy.com/software/BeautifulSoup/ + +Beautiful Soup parses a (possibly invalid) XML or HTML document into a +tree representation. It provides methods and Pythonic idioms that make +it easy to navigate, search, and modify the tree. + +A well-formed XML/HTML document yields a well-formed data +structure. An ill-formed XML/HTML document yields a correspondingly +ill-formed data structure. If your document is only locally +well-formed, you can use this library to find and process the +well-formed part of it. + +Beautiful Soup works with Python 2.2 and up. It has no external +dependencies, but you'll have more success at converting data to UTF-8 +if you also install these three packages: + +* chardet, for auto-detecting character encodings + http://chardet.feedparser.org/ +* cjkcodecs and iconv_codec, which add more encodings to the ones supported + by stock Python. + http://cjkpython.i18n.org/ + +Beautiful Soup defines classes for two main parsing strategies: + + * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific + language that kind of looks like XML. + + * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid + or invalid. This class has web browser-like heuristics for + obtaining a sensible parse tree in the face of common HTML errors. + +Beautiful Soup also defines a class (UnicodeDammit) for autodetecting +the encoding of an HTML or XML document, and converting it to +Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser. + +For more than you ever wanted to know about Beautiful Soup, see the +documentation: +http://www.crummy.com/software/BeautifulSoup/documentation.html + +Here, have some legalese: + +Copyright (c) 2004-2010, Leonard Richardson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the the Beautiful Soup Consortium and All + Night Kosher Bakery nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. + +""" +from __future__ import generators + +__author__ = "Leonard Richardson (leonardr@segfault.org)" +__version__ = "3.2.0" +__copyright__ = "Copyright (c) 2004-2010 Leonard Richardson" +__license__ = "New-style BSD" + +from sgmllib import SGMLParser, SGMLParseError +import codecs +import markupbase +import types +import re +import sgmllib +try: + from htmlentitydefs import name2codepoint +except ImportError: + name2codepoint = {} +try: + set +except NameError: + from sets import Set as set + +#These hacks make Beautiful Soup able to parse XML with namespaces +sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') +markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match + +DEFAULT_OUTPUT_ENCODING = "utf-8" + +def _match_css_class(str): + """Build a RE to match the given CSS class.""" + return re.compile(r"(^|.*\s)%s($|\s)" % str) + +# First, the classes that represent markup elements. + +class PageElement(object): + """Contains the navigational information for some part of the page + (either a tag or a piece of text)""" + + def setup(self, parent=None, previous=None): + """Sets up the initial relations between this element and + other elements.""" + self.parent = parent + self.previous = previous + self.next = None + self.previousSibling = None + self.nextSibling = None + if self.parent and self.parent.contents: + self.previousSibling = self.parent.contents[-1] + self.previousSibling.nextSibling = self + + def replaceWith(self, replaceWith): + oldParent = self.parent + myIndex = self.parent.index(self) + if hasattr(replaceWith, "parent")\ + and replaceWith.parent is self.parent: + # We're replacing this element with one of its siblings. + index = replaceWith.parent.index(replaceWith) + if index and index < myIndex: + # Furthermore, it comes before this element. That + # means that when we extract it, the index of this + # element will change. + myIndex = myIndex - 1 + self.extract() + oldParent.insert(myIndex, replaceWith) + + def replaceWithChildren(self): + myParent = self.parent + myIndex = self.parent.index(self) + self.extract() + reversedChildren = list(self.contents) + reversedChildren.reverse() + for child in reversedChildren: + myParent.insert(myIndex, child) + + def extract(self): + """Destructively rips this element out of the tree.""" + if self.parent: + try: + del self.parent.contents[self.parent.index(self)] + except ValueError: + pass + + #Find the two elements that would be next to each other if + #this element (and any children) hadn't been parsed. Connect + #the two. + lastChild = self._lastRecursiveChild() + nextElement = lastChild.next + + if self.previous: + self.previous.next = nextElement + if nextElement: + nextElement.previous = self.previous + self.previous = None + lastChild.next = None + + self.parent = None + if self.previousSibling: + self.previousSibling.nextSibling = self.nextSibling + if self.nextSibling: + self.nextSibling.previousSibling = self.previousSibling + self.previousSibling = self.nextSibling = None + return self + + def _lastRecursiveChild(self): + "Finds the last element beneath this object to be parsed." + lastChild = self + while hasattr(lastChild, 'contents') and lastChild.contents: + lastChild = lastChild.contents[-1] + return lastChild + + def insert(self, position, newChild): + if isinstance(newChild, basestring) \ + and not isinstance(newChild, NavigableString): + newChild = NavigableString(newChild) + + position = min(position, len(self.contents)) + if hasattr(newChild, 'parent') and newChild.parent is not None: + # We're 'inserting' an element that's already one + # of this object's children. + if newChild.parent is self: + index = self.index(newChild) + if index > position: + # Furthermore we're moving it further down the + # list of this object's children. That means that + # when we extract this element, our target index + # will jump down one. + position = position - 1 + newChild.extract() + + newChild.parent = self + previousChild = None + if position == 0: + newChild.previousSibling = None + newChild.previous = self + else: + previousChild = self.contents[position-1] + newChild.previousSibling = previousChild + newChild.previousSibling.nextSibling = newChild + newChild.previous = previousChild._lastRecursiveChild() + if newChild.previous: + newChild.previous.next = newChild + + newChildsLastElement = newChild._lastRecursiveChild() + + if position >= len(self.contents): + newChild.nextSibling = None + + parent = self + parentsNextSibling = None + while not parentsNextSibling: + parentsNextSibling = parent.nextSibling + parent = parent.parent + if not parent: # This is the last element in the document. + break + if parentsNextSibling: + newChildsLastElement.next = parentsNextSibling + else: + newChildsLastElement.next = None + else: + nextChild = self.contents[position] + newChild.nextSibling = nextChild + if newChild.nextSibling: + newChild.nextSibling.previousSibling = newChild + newChildsLastElement.next = nextChild + + if newChildsLastElement.next: + newChildsLastElement.next.previous = newChildsLastElement + self.contents.insert(position, newChild) + + def append(self, tag): + """Appends the given tag to the contents of this tag.""" + self.insert(len(self.contents), tag) + + def findNext(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears after this Tag in the document.""" + return self._findOne(self.findAllNext, name, attrs, text, **kwargs) + + def findAllNext(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.nextGenerator, + **kwargs) + + def findNextSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears after this Tag in the document.""" + return self._findOne(self.findNextSiblings, name, attrs, text, + **kwargs) + + def findNextSiblings(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.nextSiblingGenerator, **kwargs) + fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x + + def findPrevious(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears before this Tag in the document.""" + return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs) + + def findAllPrevious(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.previousGenerator, + **kwargs) + fetchPrevious = findAllPrevious # Compatibility with pre-3.x + + def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears before this Tag in the document.""" + return self._findOne(self.findPreviousSiblings, name, attrs, text, + **kwargs) + + def findPreviousSiblings(self, name=None, attrs={}, text=None, + limit=None, **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.previousSiblingGenerator, **kwargs) + fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x + + def findParent(self, name=None, attrs={}, **kwargs): + """Returns the closest parent of this Tag that matches the given + criteria.""" + # NOTE: We can't use _findOne because findParents takes a different + # set of arguments. + r = None + l = self.findParents(name, attrs, 1) + if l: + r = l[0] + return r + + def findParents(self, name=None, attrs={}, limit=None, **kwargs): + """Returns the parents of this Tag that match the given + criteria.""" + + return self._findAll(name, attrs, None, limit, self.parentGenerator, + **kwargs) + fetchParents = findParents # Compatibility with pre-3.x + + #These methods do the real heavy lifting. + + def _findOne(self, method, name, attrs, text, **kwargs): + r = None + l = method(name, attrs, text, 1, **kwargs) + if l: + r = l[0] + return r + + def _findAll(self, name, attrs, text, limit, generator, **kwargs): + "Iterates over a generator looking for things that match." + + if isinstance(name, SoupStrainer): + strainer = name + # (Possibly) special case some findAll*(...) searches + elif text is None and not limit and not attrs and not kwargs: + # findAll*(True) + if name is True: + return [element for element in generator() + if isinstance(element, Tag)] + # findAll*('tag-name') + elif isinstance(name, basestring): + return [element for element in generator() + if isinstance(element, Tag) and + element.name == name] + else: + strainer = SoupStrainer(name, attrs, text, **kwargs) + # Build a SoupStrainer + else: + strainer = SoupStrainer(name, attrs, text, **kwargs) + results = ResultSet(strainer) + g = generator() + while True: + try: + i = g.next() + except StopIteration: + break + if i: + found = strainer.search(i) + if found: + results.append(found) + if limit and len(results) >= limit: + break + return results + + #These Generators can be used to navigate starting from both + #NavigableStrings and Tags. + def nextGenerator(self): + i = self + while i is not None: + i = i.next + yield i + + def nextSiblingGenerator(self): + i = self + while i is not None: + i = i.nextSibling + yield i + + def previousGenerator(self): + i = self + while i is not None: + i = i.previous + yield i + + def previousSiblingGenerator(self): + i = self + while i is not None: + i = i.previousSibling + yield i + + def parentGenerator(self): + i = self + while i is not None: + i = i.parent + yield i + + # Utility methods + def substituteEncoding(self, str, encoding=None): + encoding = encoding or "utf-8" + return str.replace("%SOUP-ENCODING%", encoding) + + def toEncoding(self, s, encoding=None): + """Encodes an object to a string in some encoding, or to Unicode. + .""" + if isinstance(s, unicode): + if encoding: + s = s.encode(encoding) + elif isinstance(s, str): + if encoding: + s = s.encode(encoding) + else: + s = unicode(s) + else: + if encoding: + s = self.toEncoding(str(s), encoding) + else: + s = unicode(s) + return s + +class NavigableString(unicode, PageElement): + + def __new__(cls, value): + """Create a new NavigableString. + + When unpickling a NavigableString, this method is called with + the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be + passed in to the superclass's __new__ or the superclass won't know + how to handle non-ASCII characters. + """ + if isinstance(value, unicode): + return unicode.__new__(cls, value) + return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING) + + def __getnewargs__(self): + return (NavigableString.__str__(self),) + + def __getattr__(self, attr): + """text.string gives you text. This is for backwards + compatibility for Navigable*String, but for CData* it lets you + get the string without the CData wrapper.""" + if attr == 'string': + return self + else: + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) + + def __unicode__(self): + return str(self).decode(DEFAULT_OUTPUT_ENCODING) + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + if encoding: + return self.encode(encoding) + else: + return self + +class CData(NavigableString): + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class ProcessingInstruction(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + output = self + if "%SOUP-ENCODING%" in output: + output = self.substituteEncoding(output, encoding) + return "" % self.toEncoding(output, encoding) + +class Comment(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class Declaration(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class Tag(PageElement): + + """Represents a found HTML tag with its attributes and contents.""" + + def _invert(h): + "Cheap function to invert a hash." + i = {} + for k,v in h.items(): + i[v] = k + return i + + XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'", + "quot" : '"', + "amp" : "&", + "lt" : "<", + "gt" : ">" } + + XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS) + + def _convertEntities(self, match): + """Used in a call to re.sub to replace HTML, XML, and numeric + entities with the appropriate Unicode characters. If HTML + entities are being converted, any unrecognized entities are + escaped.""" + x = match.group(1) + if self.convertHTMLEntities and x in name2codepoint: + return unichr(name2codepoint[x]) + elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS: + if self.convertXMLEntities: + return self.XML_ENTITIES_TO_SPECIAL_CHARS[x] + else: + return u'&%s;' % x + elif len(x) > 0 and x[0] == '#': + # Handle numeric entities + if len(x) > 1 and x[1] == 'x': + return unichr(int(x[2:], 16)) + else: + return unichr(int(x[1:])) + + elif self.escapeUnrecognizedEntities: + return u'&%s;' % x + else: + return u'&%s;' % x + + def __init__(self, parser, name, attrs=None, parent=None, + previous=None): + "Basic constructor." + + # We don't actually store the parser object: that lets extracted + # chunks be garbage-collected + self.parserClass = parser.__class__ + self.isSelfClosing = parser.isSelfClosingTag(name) + self.name = name + if attrs is None: + attrs = [] + elif isinstance(attrs, dict): + attrs = attrs.items() + self.attrs = attrs + self.contents = [] + self.setup(parent, previous) + self.hidden = False + self.containsSubstitutions = False + self.convertHTMLEntities = parser.convertHTMLEntities + self.convertXMLEntities = parser.convertXMLEntities + self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities + + # Convert any HTML, XML, or numeric entities in the attribute values. + convert = lambda(k, val): (k, + re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);", + self._convertEntities, + val)) + self.attrs = map(convert, self.attrs) + + def getString(self): + if (len(self.contents) == 1 + and isinstance(self.contents[0], NavigableString)): + return self.contents[0] + + def setString(self, string): + """Replace the contents of the tag with a string""" + self.clear() + self.append(string) + + string = property(getString, setString) + + def getText(self, separator=u""): + if not len(self.contents): + return u"" + stopNode = self._lastRecursiveChild().next + strings = [] + current = self.contents[0] + while current is not stopNode: + if isinstance(current, NavigableString): + strings.append(current.strip()) + current = current.next + return separator.join(strings) + + text = property(getText) + + def get(self, key, default=None): + """Returns the value of the 'key' attribute for the tag, or + the value given for 'default' if it doesn't have that + attribute.""" + return self._getAttrMap().get(key, default) + + def clear(self): + """Extract all children.""" + for child in self.contents[:]: + child.extract() + + def index(self, element): + for i, child in enumerate(self.contents): + if child is element: + return i + raise ValueError("Tag.index: element not in tag") + + def has_key(self, key): + return self._getAttrMap().has_key(key) + + def __getitem__(self, key): + """tag[key] returns the value of the 'key' attribute for the tag, + and throws an exception if it's not there.""" + return self._getAttrMap()[key] + + def __iter__(self): + "Iterating over a tag iterates over its contents." + return iter(self.contents) + + def __len__(self): + "The length of a tag is the length of its list of contents." + return len(self.contents) + + def __contains__(self, x): + return x in self.contents + + def __nonzero__(self): + "A tag is non-None even if it has no contents." + return True + + def __setitem__(self, key, value): + """Setting tag[key] sets the value of the 'key' attribute for the + tag.""" + self._getAttrMap() + self.attrMap[key] = value + found = False + for i in range(0, len(self.attrs)): + if self.attrs[i][0] == key: + self.attrs[i] = (key, value) + found = True + if not found: + self.attrs.append((key, value)) + self._getAttrMap()[key] = value + + def __delitem__(self, key): + "Deleting tag[key] deletes all 'key' attributes for the tag." + for item in self.attrs: + if item[0] == key: + self.attrs.remove(item) + #We don't break because bad HTML can define the same + #attribute multiple times. + self._getAttrMap() + if self.attrMap.has_key(key): + del self.attrMap[key] + + def __call__(self, *args, **kwargs): + """Calling a tag like a function is the same as calling its + findAll() method. Eg. tag('a') returns a list of all the A tags + found within this tag.""" + return apply(self.findAll, args, kwargs) + + def __getattr__(self, tag): + #print "Getattr %s.%s" % (self.__class__, tag) + if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: + return self.find(tag[:-3]) + elif tag.find('__') != 0: + return self.find(tag) + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag) + + def __eq__(self, other): + """Returns true iff this tag has the same name, the same attributes, + and the same contents (recursively) as the given tag. + + NOTE: right now this will return false if two tags have the + same attributes in a different order. Should this be fixed?""" + if other is self: + return True + if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other): + return False + for i in range(0, len(self.contents)): + if self.contents[i] != other.contents[i]: + return False + return True + + def __ne__(self, other): + """Returns true iff this tag is not identical to the other tag, + as defined in __eq__.""" + return not self == other + + def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING): + """Renders this tag as a string.""" + return self.__str__(encoding) + + def __unicode__(self): + return self.__str__(None) + + BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" + + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" + + ")") + + def _sub_entity(self, x): + """Used with a regular expression to substitute the + appropriate XML entity for an XML special character.""" + return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";" + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Returns a string or Unicode representation of this tag and + its contents. To get Unicode, pass None for encoding. + + NOTE: since Python's HTML parser consumes whitespace, this + method is not certain to reproduce the whitespace present in + the original string.""" + + encodedName = self.toEncoding(self.name, encoding) + + attrs = [] + if self.attrs: + for key, val in self.attrs: + fmt = '%s="%s"' + if isinstance(val, basestring): + if self.containsSubstitutions and '%SOUP-ENCODING%' in val: + val = self.substituteEncoding(val, encoding) + + # The attribute value either: + # + # * Contains no embedded double quotes or single quotes. + # No problem: we enclose it in double quotes. + # * Contains embedded single quotes. No problem: + # double quotes work here too. + # * Contains embedded double quotes. No problem: + # we enclose it in single quotes. + # * Embeds both single _and_ double quotes. This + # can't happen naturally, but it can happen if + # you modify an attribute value after parsing + # the document. Now we have a bit of a + # problem. We solve it by enclosing the + # attribute in single quotes, and escaping any + # embedded single quotes to XML entities. + if '"' in val: + fmt = "%s='%s'" + if "'" in val: + # TODO: replace with apos when + # appropriate. + val = val.replace("'", "&squot;") + + # Now we're okay w/r/t quotes. But the attribute + # value might also contain angle brackets, or + # ampersands that aren't part of entities. We need + # to escape those to XML entities too. + val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val) + + attrs.append(fmt % (self.toEncoding(key, encoding), + self.toEncoding(val, encoding))) + close = '' + closeTag = '' + if self.isSelfClosing: + close = ' /' + else: + closeTag = '' % encodedName + + indentTag, indentContents = 0, 0 + if prettyPrint: + indentTag = indentLevel + space = (' ' * (indentTag-1)) + indentContents = indentTag + 1 + contents = self.renderContents(encoding, prettyPrint, indentContents) + if self.hidden: + s = contents + else: + s = [] + attributeString = '' + if attrs: + attributeString = ' ' + ' '.join(attrs) + if prettyPrint: + s.append(space) + s.append('<%s%s%s>' % (encodedName, attributeString, close)) + if prettyPrint: + s.append("\n") + s.append(contents) + if prettyPrint and contents and contents[-1] != "\n": + s.append("\n") + if prettyPrint and closeTag: + s.append(space) + s.append(closeTag) + if prettyPrint and closeTag and self.nextSibling: + s.append("\n") + s = ''.join(s) + return s + + def decompose(self): + """Recursively destroys the contents of this tree.""" + self.extract() + if len(self.contents) == 0: + return + current = self.contents[0] + while current is not None: + next = current.next + if isinstance(current, Tag): + del current.contents[:] + current.parent = None + current.previous = None + current.previousSibling = None + current.next = None + current.nextSibling = None + current = next + + def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): + return self.__str__(encoding, True) + + def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Renders the contents of this tag as a string in the given + encoding. If encoding is None, returns a Unicode string..""" + s=[] + for c in self: + text = None + if isinstance(c, NavigableString): + text = c.__str__(encoding) + elif isinstance(c, Tag): + s.append(c.__str__(encoding, prettyPrint, indentLevel)) + if text and prettyPrint: + text = text.strip() + if text: + if prettyPrint: + s.append(" " * (indentLevel-1)) + s.append(text) + if prettyPrint: + s.append("\n") + return ''.join(s) + + #Soup methods + + def find(self, name=None, attrs={}, recursive=True, text=None, + **kwargs): + """Return only the first child of this Tag matching the given + criteria.""" + r = None + l = self.findAll(name, attrs, recursive, text, 1, **kwargs) + if l: + r = l[0] + return r + findChild = find + + def findAll(self, name=None, attrs={}, recursive=True, text=None, + limit=None, **kwargs): + """Extracts a list of Tag objects that match the given + criteria. You can specify the name of the Tag and any + attributes you want the Tag to have. + + The value of a key-value pair in the 'attrs' map can be a + string, a list of strings, a regular expression object, or a + callable that takes a string and returns whether or not the + string matches for some custom definition of 'matches'. The + same is true of the tag name.""" + generator = self.recursiveChildGenerator + if not recursive: + generator = self.childGenerator + return self._findAll(name, attrs, text, limit, generator, **kwargs) + findChildren = findAll + + # Pre-3.x compatibility methods + first = find + fetch = findAll + + def fetchText(self, text=None, recursive=True, limit=None): + return self.findAll(text=text, recursive=recursive, limit=limit) + + def firstText(self, text=None, recursive=True): + return self.find(text=text, recursive=recursive) + + #Private methods + + def _getAttrMap(self): + """Initializes a map representation of this tag's attributes, + if not already initialized.""" + if not getattr(self, 'attrMap'): + self.attrMap = {} + for (key, value) in self.attrs: + self.attrMap[key] = value + return self.attrMap + + #Generator methods + def childGenerator(self): + # Just use the iterator from the contents + return iter(self.contents) + + def recursiveChildGenerator(self): + if not len(self.contents): + raise StopIteration + stopNode = self._lastRecursiveChild().next + current = self.contents[0] + while current is not stopNode: + yield current + current = current.next + + +# Next, a couple classes to represent queries and their results. +class SoupStrainer: + """Encapsulates a number of ways of matching a markup element (tag or + text).""" + + def __init__(self, name=None, attrs={}, text=None, **kwargs): + self.name = name + if isinstance(attrs, basestring): + kwargs['class'] = _match_css_class(attrs) + attrs = None + if kwargs: + if attrs: + attrs = attrs.copy() + attrs.update(kwargs) + else: + attrs = kwargs + self.attrs = attrs + self.text = text + + def __str__(self): + if self.text: + return self.text + else: + return "%s|%s" % (self.name, self.attrs) + + def searchTag(self, markupName=None, markupAttrs={}): + found = None + markup = None + if isinstance(markupName, Tag): + markup = markupName + markupAttrs = markup + callFunctionWithTagData = callable(self.name) \ + and not isinstance(markupName, Tag) + + if (not self.name) \ + or callFunctionWithTagData \ + or (markup and self._matches(markup, self.name)) \ + or (not markup and self._matches(markupName, self.name)): + if callFunctionWithTagData: + match = self.name(markupName, markupAttrs) + else: + match = True + markupAttrMap = None + for attr, matchAgainst in self.attrs.items(): + if not markupAttrMap: + if hasattr(markupAttrs, 'get'): + markupAttrMap = markupAttrs + else: + markupAttrMap = {} + for k,v in markupAttrs: + markupAttrMap[k] = v + attrValue = markupAttrMap.get(attr) + if not self._matches(attrValue, matchAgainst): + match = False + break + if match: + if markup: + found = markup + else: + found = markupName + return found + + def search(self, markup): + #print 'looking for %s in %s' % (self, markup) + found = None + # If given a list of items, scan it for a text element that + # matches. + if hasattr(markup, "__iter__") \ + and not isinstance(markup, Tag): + for element in markup: + if isinstance(element, NavigableString) \ + and self.search(element): + found = element + break + # If it's a Tag, make sure its name or attributes match. + # Don't bother with Tags if we're searching for text. + elif isinstance(markup, Tag): + if not self.text: + found = self.searchTag(markup) + # If it's text, make sure the text matches. + elif isinstance(markup, NavigableString) or \ + isinstance(markup, basestring): + if self._matches(markup, self.text): + found = markup + else: + raise Exception, "I don't know how to match against a %s" \ + % markup.__class__ + return found + + def _matches(self, markup, matchAgainst): + #print "Matching %s against %s" % (markup, matchAgainst) + result = False + if matchAgainst is True: + result = markup is not None + elif callable(matchAgainst): + result = matchAgainst(markup) + else: + #Custom match methods take the tag as an argument, but all + #other ways of matching match the tag name as a string. + if isinstance(markup, Tag): + markup = markup.name + if markup and not isinstance(markup, basestring): + markup = unicode(markup) + #Now we know that chunk is either a string, or None. + if hasattr(matchAgainst, 'match'): + # It's a regexp object. + result = markup and matchAgainst.search(markup) + elif hasattr(matchAgainst, '__iter__'): # list-like + result = markup in matchAgainst + elif hasattr(matchAgainst, 'items'): + result = markup.has_key(matchAgainst) + elif matchAgainst and isinstance(markup, basestring): + if isinstance(markup, unicode): + matchAgainst = unicode(matchAgainst) + else: + matchAgainst = str(matchAgainst) + + if not result: + result = matchAgainst == markup + return result + +class ResultSet(list): + """A ResultSet is just a list that keeps track of the SoupStrainer + that created it.""" + def __init__(self, source): + list.__init__([]) + self.source = source + +# Now, some helper functions. + +def buildTagMap(default, *args): + """Turns a list of maps, lists, or scalars into a single map. + Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and + NESTING_RESET_TAGS maps out of lists and partial maps.""" + built = {} + for portion in args: + if hasattr(portion, 'items'): + #It's a map. Merge it. + for k,v in portion.items(): + built[k] = v + elif hasattr(portion, '__iter__'): # is a list + #It's a list. Map each item to the default. + for k in portion: + built[k] = default + else: + #It's a scalar. Map it to the default. + built[portion] = default + return built + +# Now, the parser classes. + +class BeautifulStoneSoup(Tag, SGMLParser): + + """This class contains the basic parser and search code. It defines + a parser that knows nothing about tag behavior except for the + following: + + You can't close a tag without closing all the tags it encloses. + That is, "" actually means + "". + + [Another possible explanation is "", but since + this class defines no SELF_CLOSING_TAGS, it will never use that + explanation.] + + This class is useful for parsing XML or made-up markup languages, + or when BeautifulSoup makes an assumption counter to what you were + expecting.""" + + SELF_CLOSING_TAGS = {} + NESTABLE_TAGS = {} + RESET_NESTING_TAGS = {} + QUOTE_TAGS = {} + PRESERVE_WHITESPACE_TAGS = [] + + MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'), + lambda x: x.group(1) + ' />'), + (re.compile(']*)>'), + lambda x: '') + ] + + ROOT_TAG_NAME = u'[document]' + + HTML_ENTITIES = "html" + XML_ENTITIES = "xml" + XHTML_ENTITIES = "xhtml" + # TODO: This only exists for backwards-compatibility + ALL_ENTITIES = XHTML_ENTITIES + + # Used when determining whether a text node is all whitespace and + # can be replaced with a single space. A text node that contains + # fancy Unicode spaces (usually non-breaking) should be left + # alone. + STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, } + + def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None, + markupMassage=True, smartQuotesTo=XML_ENTITIES, + convertEntities=None, selfClosingTags=None, isHTML=False): + """The Soup object is initialized as the 'root tag', and the + provided markup (which can be a string or a file-like object) + is fed into the underlying parser. + + sgmllib will process most bad HTML, and the BeautifulSoup + class has some tricks for dealing with some HTML that kills + sgmllib, but Beautiful Soup can nonetheless choke or lose data + if your data uses self-closing tags or declarations + incorrectly. + + By default, Beautiful Soup uses regexes to sanitize input, + avoiding the vast majority of these problems. If the problems + don't apply to you, pass in False for markupMassage, and + you'll get better performance. + + The default parser massage techniques fix the two most common + instances of invalid HTML that choke sgmllib: + +
(No space between name of closing tag and tag close) + (Extraneous whitespace in declaration) + + You can pass in a custom list of (RE object, replace method) + tuples to get Beautiful Soup to scrub your input the way you + want.""" + + self.parseOnlyThese = parseOnlyThese + self.fromEncoding = fromEncoding + self.smartQuotesTo = smartQuotesTo + self.convertEntities = convertEntities + # Set the rules for how we'll deal with the entities we + # encounter + if self.convertEntities: + # It doesn't make sense to convert encoded characters to + # entities even while you're converting entities to Unicode. + # Just convert it all to Unicode. + self.smartQuotesTo = None + if convertEntities == self.HTML_ENTITIES: + self.convertXMLEntities = False + self.convertHTMLEntities = True + self.escapeUnrecognizedEntities = True + elif convertEntities == self.XHTML_ENTITIES: + self.convertXMLEntities = True + self.convertHTMLEntities = True + self.escapeUnrecognizedEntities = False + elif convertEntities == self.XML_ENTITIES: + self.convertXMLEntities = True + self.convertHTMLEntities = False + self.escapeUnrecognizedEntities = False + else: + self.convertXMLEntities = False + self.convertHTMLEntities = False + self.escapeUnrecognizedEntities = False + + self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags) + SGMLParser.__init__(self) + + if hasattr(markup, 'read'): # It's a file-type object. + markup = markup.read() + self.markup = markup + self.markupMassage = markupMassage + try: + self._feed(isHTML=isHTML) + except StopParsing: + pass + self.markup = None # The markup can now be GCed + + def convert_charref(self, name): + """This method fixes a bug in Python's SGMLParser.""" + try: + n = int(name) + except ValueError: + return + if not 0 <= n <= 127 : # ASCII ends at 127, not 255 + return + return self.convert_codepoint(n) + + def _feed(self, inDocumentEncoding=None, isHTML=False): + # Convert the document to Unicode. + markup = self.markup + if isinstance(markup, unicode): + if not hasattr(self, 'originalEncoding'): + self.originalEncoding = None + else: + dammit = UnicodeDammit\ + (markup, [self.fromEncoding, inDocumentEncoding], + smartQuotesTo=self.smartQuotesTo, isHTML=isHTML) + markup = dammit.unicode + self.originalEncoding = dammit.originalEncoding + self.declaredHTMLEncoding = dammit.declaredHTMLEncoding + if markup: + if self.markupMassage: + if not hasattr(self.markupMassage, "__iter__"): + self.markupMassage = self.MARKUP_MASSAGE + for fix, m in self.markupMassage: + markup = fix.sub(m, markup) + # TODO: We get rid of markupMassage so that the + # soup object can be deepcopied later on. Some + # Python installations can't copy regexes. If anyone + # was relying on the existence of markupMassage, this + # might cause problems. + del(self.markupMassage) + self.reset() + + SGMLParser.feed(self, markup) + # Close out any unfinished strings and close all the open tags. + self.endData() + while self.currentTag.name != self.ROOT_TAG_NAME: + self.popTag() + + def __getattr__(self, methodName): + """This method routes method call requests to either the SGMLParser + superclass or the Tag superclass, depending on the method name.""" + #print "__getattr__ called on %s.%s" % (self.__class__, methodName) + + if methodName.startswith('start_') or methodName.startswith('end_') \ + or methodName.startswith('do_'): + return SGMLParser.__getattr__(self, methodName) + elif not methodName.startswith('__'): + return Tag.__getattr__(self, methodName) + else: + raise AttributeError + + def isSelfClosingTag(self, name): + """Returns true iff the given string is the name of a + self-closing tag according to this parser.""" + return self.SELF_CLOSING_TAGS.has_key(name) \ + or self.instanceSelfClosingTags.has_key(name) + + def reset(self): + Tag.__init__(self, self, self.ROOT_TAG_NAME) + self.hidden = 1 + SGMLParser.reset(self) + self.currentData = [] + self.currentTag = None + self.tagStack = [] + self.quoteStack = [] + self.pushTag(self) + + def popTag(self): + tag = self.tagStack.pop() + + #print "Pop", tag.name + if self.tagStack: + self.currentTag = self.tagStack[-1] + return self.currentTag + + def pushTag(self, tag): + #print "Push", tag.name + if self.currentTag: + self.currentTag.contents.append(tag) + self.tagStack.append(tag) + self.currentTag = self.tagStack[-1] + + def endData(self, containerClass=NavigableString): + if self.currentData: + currentData = u''.join(self.currentData) + if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and + not set([tag.name for tag in self.tagStack]).intersection( + self.PRESERVE_WHITESPACE_TAGS)): + if '\n' in currentData: + currentData = '\n' + else: + currentData = ' ' + self.currentData = [] + if self.parseOnlyThese and len(self.tagStack) <= 1 and \ + (not self.parseOnlyThese.text or \ + not self.parseOnlyThese.search(currentData)): + return + o = containerClass(currentData) + o.setup(self.currentTag, self.previous) + if self.previous: + self.previous.next = o + self.previous = o + self.currentTag.contents.append(o) + + + def _popToTag(self, name, inclusivePop=True): + """Pops the tag stack up to and including the most recent + instance of the given tag. If inclusivePop is false, pops the tag + stack up to but *not* including the most recent instqance of + the given tag.""" + #print "Popping to %s" % name + if name == self.ROOT_TAG_NAME: + return + + numPops = 0 + mostRecentTag = None + for i in range(len(self.tagStack)-1, 0, -1): + if name == self.tagStack[i].name: + numPops = len(self.tagStack)-i + break + if not inclusivePop: + numPops = numPops - 1 + + for i in range(0, numPops): + mostRecentTag = self.popTag() + return mostRecentTag + + def _smartPop(self, name): + + """We need to pop up to the previous tag of this type, unless + one of this tag's nesting reset triggers comes between this + tag and the previous tag of this type, OR unless this tag is a + generic nesting trigger and another generic nesting trigger + comes between this tag and the previous tag of this type. + + Examples: +

FooBar *

* should pop to 'p', not 'b'. +

FooBar *

* should pop to 'table', not 'p'. +

Foo

Bar *

* should pop to 'tr', not 'p'. + +

    • *
    • * should pop to 'ul', not the first 'li'. +
  • ** should pop to 'table', not the first 'tr' + tag should + implicitly close the previous tag within the same
    ** should pop to 'tr', not the first 'td' + """ + + nestingResetTriggers = self.NESTABLE_TAGS.get(name) + isNestable = nestingResetTriggers != None + isResetNesting = self.RESET_NESTING_TAGS.has_key(name) + popTo = None + inclusive = True + for i in range(len(self.tagStack)-1, 0, -1): + p = self.tagStack[i] + if (not p or p.name == name) and not isNestable: + #Non-nestable tags get popped to the top or to their + #last occurance. + popTo = name + break + if (nestingResetTriggers is not None + and p.name in nestingResetTriggers) \ + or (nestingResetTriggers is None and isResetNesting + and self.RESET_NESTING_TAGS.has_key(p.name)): + + #If we encounter one of the nesting reset triggers + #peculiar to this tag, or we encounter another tag + #that causes nesting to reset, pop up to but not + #including that tag. + popTo = p.name + inclusive = False + break + p = p.parent + if popTo: + self._popToTag(popTo, inclusive) + + def unknown_starttag(self, name, attrs, selfClosing=0): + #print "Start tag %s: %s" % (name, attrs) + if self.quoteStack: + #This is not a real tag. + #print "<%s> is not real!" % name + attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs]) + self.handle_data('<%s%s>' % (name, attrs)) + return + self.endData() + + if not self.isSelfClosingTag(name) and not selfClosing: + self._smartPop(name) + + if self.parseOnlyThese and len(self.tagStack) <= 1 \ + and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)): + return + + tag = Tag(self, name, attrs, self.currentTag, self.previous) + if self.previous: + self.previous.next = tag + self.previous = tag + self.pushTag(tag) + if selfClosing or self.isSelfClosingTag(name): + self.popTag() + if name in self.QUOTE_TAGS: + #print "Beginning quote (%s)" % name + self.quoteStack.append(name) + self.literal = 1 + return tag + + def unknown_endtag(self, name): + #print "End tag %s" % name + if self.quoteStack and self.quoteStack[-1] != name: + #This is not a real end tag. + #print " is not real!" % name + self.handle_data('' % name) + return + self.endData() + self._popToTag(name) + if self.quoteStack and self.quoteStack[-1] == name: + self.quoteStack.pop() + self.literal = (len(self.quoteStack) > 0) + + def handle_data(self, data): + self.currentData.append(data) + + def _toStringSubclass(self, text, subclass): + """Adds a certain piece of text to the tree as a NavigableString + subclass.""" + self.endData() + self.handle_data(text) + self.endData(subclass) + + def handle_pi(self, text): + """Handle a processing instruction as a ProcessingInstruction + object, possibly one with a %SOUP-ENCODING% slot into which an + encoding will be plugged later.""" + if text[:3] == "xml": + text = u"xml version='1.0' encoding='%SOUP-ENCODING%'" + self._toStringSubclass(text, ProcessingInstruction) + + def handle_comment(self, text): + "Handle comments as Comment objects." + self._toStringSubclass(text, Comment) + + def handle_charref(self, ref): + "Handle character references as data." + if self.convertEntities: + data = unichr(int(ref)) + else: + data = '&#%s;' % ref + self.handle_data(data) + + def handle_entityref(self, ref): + """Handle entity references as data, possibly converting known + HTML and/or XML entity references to the corresponding Unicode + characters.""" + data = None + if self.convertHTMLEntities: + try: + data = unichr(name2codepoint[ref]) + except KeyError: + pass + + if not data and self.convertXMLEntities: + data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref) + + if not data and self.convertHTMLEntities and \ + not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref): + # TODO: We've got a problem here. We're told this is + # an entity reference, but it's not an XML entity + # reference or an HTML entity reference. Nonetheless, + # the logical thing to do is to pass it through as an + # unrecognized entity reference. + # + # Except: when the input is "&carol;" this function + # will be called with input "carol". When the input is + # "AT&T", this function will be called with input + # "T". We have no way of knowing whether a semicolon + # was present originally, so we don't know whether + # this is an unknown entity or just a misplaced + # ampersand. + # + # The more common case is a misplaced ampersand, so I + # escape the ampersand and omit the trailing semicolon. + data = "&%s" % ref + if not data: + # This case is different from the one above, because we + # haven't already gone through a supposedly comprehensive + # mapping of entities to Unicode characters. We might not + # have gone through any mapping at all. So the chances are + # very high that this is a real entity, and not a + # misplaced ampersand. + data = "&%s;" % ref + self.handle_data(data) + + def handle_decl(self, data): + "Handle DOCTYPEs and the like as Declaration objects." + self._toStringSubclass(data, Declaration) + + def parse_declaration(self, i): + """Treat a bogus SGML declaration as raw data. Treat a CDATA + declaration as a CData object.""" + j = None + if self.rawdata[i:i+9] == '', i) + if k == -1: + k = len(self.rawdata) + data = self.rawdata[i+9:k] + j = k+3 + self._toStringSubclass(data, CData) + else: + try: + j = SGMLParser.parse_declaration(self, i) + except SGMLParseError: + toHandle = self.rawdata[i:] + self.handle_data(toHandle) + j = i + len(toHandle) + return j + +class BeautifulSoup(BeautifulStoneSoup): + + """This parser knows the following facts about HTML: + + * Some tags have no closing tag and should be interpreted as being + closed as soon as they are encountered. + + * The text inside some tags (ie. 'script') may contain tags which + are not really part of the document and which should be parsed + as text, not tags. If you want to parse the text as tags, you can + always fetch it and parse it explicitly. + + * Tag nesting rules: + + Most tags can't be nested at all. For instance, the occurance of + a

    tag should implicitly close the previous

    tag. + +

    Para1

    Para2 + should be transformed into: +

    Para1

    Para2 + + Some tags can be nested arbitrarily. For instance, the occurance + of a

    tag should _not_ implicitly close the previous +
    tag. + + Alice said:
    Bob said:
    Blah + should NOT be transformed into: + Alice said:
    Bob said:
    Blah + + Some tags can be nested, but the nesting is reset by the + interposition of other tags. For instance, a
    , + but not close a tag in another table. + +
    BlahBlah + should be transformed into: +
    BlahBlah + but, + Blah
    Blah + should NOT be transformed into + Blah
    Blah + + Differing assumptions about tag nesting rules are a major source + of problems with the BeautifulSoup class. If BeautifulSoup is not + treating as nestable a tag your page author treats as nestable, + try ICantBelieveItsBeautifulSoup, MinimalSoup, or + BeautifulStoneSoup before writing your own subclass.""" + + def __init__(self, *args, **kwargs): + if not kwargs.has_key('smartQuotesTo'): + kwargs['smartQuotesTo'] = self.HTML_ENTITIES + kwargs['isHTML'] = True + BeautifulStoneSoup.__init__(self, *args, **kwargs) + + SELF_CLOSING_TAGS = buildTagMap(None, + ('br' , 'hr', 'input', 'img', 'meta', + 'spacer', 'link', 'frame', 'base', 'col')) + + PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea']) + + QUOTE_TAGS = {'script' : None, 'textarea' : None} + + #According to the HTML standard, each of these inline tags can + #contain another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup', + 'center') + + #According to the HTML standard, these block tags can contain + #another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del') + + #Lists can contain other lists, but there are restrictions. + NESTABLE_LIST_TAGS = { 'ol' : [], + 'ul' : [], + 'li' : ['ul', 'ol'], + 'dl' : [], + 'dd' : ['dl'], + 'dt' : ['dl'] } + + #Tables can contain other tables, but there are restrictions. + NESTABLE_TABLE_TAGS = {'table' : [], + 'tr' : ['table', 'tbody', 'tfoot', 'thead'], + 'td' : ['tr'], + 'th' : ['tr'], + 'thead' : ['table'], + 'tbody' : ['table'], + 'tfoot' : ['table'], + } + + NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre') + + #If one of these tags is encountered, all tags up to the next tag of + #this type are popped. + RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript', + NON_NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, + NESTABLE_TABLE_TAGS) + + NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) + + # Used to detect the charset in a META tag; see start_meta + CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) + + def start_meta(self, attrs): + """Beautiful Soup can detect a charset included in a META tag, + try to convert the document to that charset, and re-parse the + document from the beginning.""" + httpEquiv = None + contentType = None + contentTypeIndex = None + tagNeedsEncodingSubstitution = False + + for i in range(0, len(attrs)): + key, value = attrs[i] + key = key.lower() + if key == 'http-equiv': + httpEquiv = value + elif key == 'content': + contentType = value + contentTypeIndex = i + + if httpEquiv and contentType: # It's an interesting meta tag. + match = self.CHARSET_RE.search(contentType) + if match: + if (self.declaredHTMLEncoding is not None or + self.originalEncoding == self.fromEncoding): + # An HTML encoding was sniffed while converting + # the document to Unicode, or an HTML encoding was + # sniffed during a previous pass through the + # document, or an encoding was specified + # explicitly and it worked. Rewrite the meta tag. + def rewrite(match): + return match.group(1) + "%SOUP-ENCODING%" + newAttr = self.CHARSET_RE.sub(rewrite, contentType) + attrs[contentTypeIndex] = (attrs[contentTypeIndex][0], + newAttr) + tagNeedsEncodingSubstitution = True + else: + # This is our first pass through the document. + # Go through it again with the encoding information. + newCharset = match.group(3) + if newCharset and newCharset != self.originalEncoding: + self.declaredHTMLEncoding = newCharset + self._feed(self.declaredHTMLEncoding) + raise StopParsing + pass + tag = self.unknown_starttag("meta", attrs) + if tag and tagNeedsEncodingSubstitution: + tag.containsSubstitutions = True + +class StopParsing(Exception): + pass + +class ICantBelieveItsBeautifulSoup(BeautifulSoup): + + """The BeautifulSoup class is oriented towards skipping over + common HTML errors like unclosed tags. However, sometimes it makes + errors of its own. For instance, consider this fragment: + + FooBar + + This is perfectly valid (if bizarre) HTML. However, the + BeautifulSoup class will implicitly close the first b tag when it + encounters the second 'b'. It will think the author wrote + "FooBar", and didn't close the first 'b' tag, because + there's no real-world reason to bold something that's already + bold. When it encounters '' it will close two more 'b' + tags, for a grand total of three tags closed instead of two. This + can throw off the rest of your document structure. The same is + true of a number of other tags, listed below. + + It's much more common for someone to forget to close a 'b' tag + than to actually use nested 'b' tags, and the BeautifulSoup class + handles the common case. This class handles the not-co-common + case: where you can't believe someone wrote what they did, but + it's valid HTML and BeautifulSoup screwed up by assuming it + wouldn't be.""" + + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \ + ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong', + 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b', + 'big') + + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',) + + NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS) + +class MinimalSoup(BeautifulSoup): + """The MinimalSoup class is for parsing HTML that contains + pathologically bad markup. It makes no assumptions about tag + nesting, but it does know which tags are self-closing, that + + + + + + diff --git a/static/js/libs/zepto.js b/static/js/libs/zepto.js new file mode 100644 index 0000000..fa0bf80 --- /dev/null +++ b/static/js/libs/zepto.js @@ -0,0 +1,1412 @@ +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. + +(function(undefined){ + if (String.prototype.trim === undefined) // fix for iOS 3.2 + String.prototype.trim = function(){ return this.replace(/^\s+/, '').replace(/\s+$/, '') }; + + // For iOS 3.x + // from https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduce + if (Array.prototype.reduce === undefined) + Array.prototype.reduce = function(fun){ + if(this === void 0 || this === null) throw new TypeError(); + var t = Object(this), len = t.length >>> 0, k = 0, accumulator; + if(typeof fun != 'function') throw new TypeError(); + if(len == 0 && arguments.length == 1) throw new TypeError(); + + if(arguments.length >= 2) + accumulator = arguments[1]; + else + do{ + if(k in t){ + accumulator = t[k++]; + break; + } + if(++k >= len) throw new TypeError(); + } while (true); + + while (k < len){ + if(k in t) accumulator = fun.call(undefined, accumulator, t[k], k, t); + k++; + } + return accumulator; + }; + +})(); +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. + +var Zepto = (function() { + var undefined, key, $$, classList, emptyArray = [], slice = emptyArray.slice, + document = window.document, + elementDisplay = {}, classCache = {}, + getComputedStyle = document.defaultView.getComputedStyle, + cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1,'opacity': 1, 'z-index': 1, 'zoom': 1 }, + fragmentRE = /^\s*<(\w+)[^>]*>/, + elementTypes = [1, 9, 11], + adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ], + table = document.createElement('table'), + tableRow = document.createElement('tr'), + containers = { + 'tr': document.createElement('tbody'), + 'tbody': table, 'thead': table, 'tfoot': table, + 'td': tableRow, 'th': tableRow, + '*': document.createElement('div') + }, + readyRE = /complete|loaded|interactive/, + classSelectorRE = /^\.([\w-]+)$/, + idSelectorRE = /^#([\w-]+)$/, + tagSelectorRE = /^[\w-]+$/; + + function isF(value) { return ({}).toString.call(value) == "[object Function]" } + function isO(value) { return value instanceof Object } + function isA(value) { return value instanceof Array } + function likeArray(obj) { return typeof obj.length == 'number' } + + function compact(array) { return array.filter(function(item){ return item !== undefined && item !== null }) } + function flatten(array) { return array.length > 0 ? [].concat.apply([], array) : array } + function camelize(str) { return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) } + function dasherize(str){ + return str.replace(/::/g, '/') + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') + .replace(/([a-z\d])([A-Z])/g, '$1_$2') + .replace(/_/g, '-') + .toLowerCase(); + } + function uniq(array) { return array.filter(function(item,index,array){ return array.indexOf(item) == index }) } + + function classRE(name){ + return name in classCache ? + classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)')); + } + + function maybeAddPx(name, value) { return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value; } + + function defaultDisplay(nodeName) { + var element, display; + if (!elementDisplay[nodeName]) { + element = document.createElement(nodeName); + document.body.appendChild(element); + display = getComputedStyle(element, '').getPropertyValue("display"); + element.parentNode.removeChild(element); + display == "none" && (display = "block"); + elementDisplay[nodeName] = display; + } + return elementDisplay[nodeName]; + } + + function fragment(html, name) { + if (name === undefined) fragmentRE.test(html) && RegExp.$1; + if (!(name in containers)) name = '*'; + var container = containers[name]; + container.innerHTML = '' + html; + return slice.call(container.childNodes); + } + + function Z(dom, selector){ + dom = dom || emptyArray; + dom.__proto__ = Z.prototype; + dom.selector = selector || ''; + return dom; + } + + function $(selector, context){ + if (!selector) return Z(); + if (context !== undefined) return $(context).find(selector); + else if (isF(selector)) return $(document).ready(selector); + else if (selector instanceof Z) return selector; + else { + var dom; + if (isA(selector)) dom = compact(selector); + else if (elementTypes.indexOf(selector.nodeType) >= 0 || selector === window) + dom = [selector], selector = null; + else if (fragmentRE.test(selector)) + dom = fragment(selector.trim(), RegExp.$1), selector = null; + else if (selector.nodeType && selector.nodeType == 3) dom = [selector]; + else dom = $$(document, selector); + return Z(dom, selector); + } + } + + $.extend = function(target){ + slice.call(arguments, 1).forEach(function(source) { + for (key in source) target[key] = source[key]; + }) + return target; + } + + $.qsa = $$ = function(element, selector){ + var found; + return (element === document && idSelectorRE.test(selector)) ? + ( (found = element.getElementById(RegExp.$1)) ? [found] : emptyArray ) : + slice.call( + classSelectorRE.test(selector) ? element.getElementsByClassName(RegExp.$1) : + tagSelectorRE.test(selector) ? element.getElementsByTagName(selector) : + element.querySelectorAll(selector) + ); + } + + function filtered(nodes, selector){ + return selector === undefined ? $(nodes) : $(nodes).filter(selector); + } + + function funcArg(context, arg, idx, payload){ + return isF(arg) ? arg.call(context, idx, payload) : arg; + } + + $.isFunction = isF; + $.isObject = isO; + $.isArray = isA; + + $.map = function(elements, callback) { + var value, values = [], i, key; + if (likeArray(elements)) + for (i = 0; i < elements.length; i++) { + value = callback(elements[i], i); + if (value != null) values.push(value); + } + else + for (key in elements) { + value = callback(elements[key], key); + if (value != null) values.push(value); + } + return flatten(values); + } + + $.each = function(elements, callback) { + var i, key; + if (likeArray(elements)) + for(i = 0; i < elements.length; i++) { + if(callback(i, elements[i]) === false) return elements; + } + else + for(key in elements) { + if(callback(key, elements[key]) === false) return elements; + } + return elements; + } + + $.fn = { + forEach: emptyArray.forEach, + reduce: emptyArray.reduce, + push: emptyArray.push, + indexOf: emptyArray.indexOf, + concat: emptyArray.concat, + map: function(fn){ + return $.map(this, function(el, i){ return fn.call(el, i, el) }); + }, + slice: function(){ + return $(slice.apply(this, arguments)); + }, + ready: function(callback){ + if (readyRE.test(document.readyState)) callback($); + else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false); + return this; + }, + get: function(idx){ return idx === undefined ? this : this[idx] }, + size: function(){ return this.length }, + remove: function () { + return this.each(function () { + if (this.parentNode != null) { + this.parentNode.removeChild(this); + } + }); + }, + each: function(callback){ + this.forEach(function(el, idx){ callback.call(el, idx, el) }); + return this; + }, + filter: function(selector){ + return $([].filter.call(this, function(element){ + return element.parentNode && $$(element.parentNode, selector).indexOf(element) >= 0; + })); + }, + end: function(){ + return this.prevObject || $(); + }, + andSelf:function(){ + return this.add(this.prevObject || $()) + }, + add:function(selector,context){ + return $(uniq(this.concat($(selector,context)))); + }, + is: function(selector){ + return this.length > 0 && $(this[0]).filter(selector).length > 0; + }, + not: function(selector){ + var nodes=[]; + if (isF(selector) && selector.call !== undefined) + this.each(function(idx){ + if (!selector.call(this,idx)) nodes.push(this); + }); + else { + var excludes = typeof selector == 'string' ? this.filter(selector) : + (likeArray(selector) && isF(selector.item)) ? slice.call(selector) : $(selector); + this.forEach(function(el){ + if (excludes.indexOf(el) < 0) nodes.push(el); + }); + } + return $(nodes); + }, + eq: function(idx){ + return idx === -1 ? this.slice(idx) : this.slice(idx, + idx + 1); + }, + first: function(){ var el = this[0]; return el && !isO(el) ? el : $(el) }, + last: function(){ var el = this[this.length - 1]; return el && !isO(el) ? el : $(el) }, + find: function(selector){ + var result; + if (this.length == 1) result = $$(this[0], selector); + else result = this.map(function(){ return $$(this, selector) }); + return $(result); + }, + closest: function(selector, context){ + var node = this[0], candidates = $$(context || document, selector); + if (!candidates.length) node = null; + while (node && candidates.indexOf(node) < 0) + node = node !== context && node !== document && node.parentNode; + return $(node); + }, + parents: function(selector){ + var ancestors = [], nodes = this; + while (nodes.length > 0) + nodes = $.map(nodes, function(node){ + if ((node = node.parentNode) && node !== document && ancestors.indexOf(node) < 0) { + ancestors.push(node); + return node; + } + }); + return filtered(ancestors, selector); + }, + parent: function(selector){ + return filtered(uniq(this.pluck('parentNode')), selector); + }, + children: function(selector){ + return filtered(this.map(function(){ return slice.call(this.children) }), selector); + }, + siblings: function(selector){ + return filtered(this.map(function(i, el){ + return slice.call(el.parentNode.children).filter(function(child){ return child!==el }); + }), selector); + }, + empty: function(){ return this.each(function(){ this.innerHTML = '' }) }, + pluck: function(property){ return this.map(function(){ return this[property] }) }, + show: function(){ + return this.each(function() { + this.style.display == "none" && (this.style.display = null); + if (getComputedStyle(this, '').getPropertyValue("display") == "none") { + this.style.display = defaultDisplay(this.nodeName) + } + }) + }, + replaceWith: function(newContent) { + return this.each(function() { + $(this).before(newContent).remove(); + }); + }, + wrap: function(newContent) { + return this.each(function() { + $(this).wrapAll($(newContent)[0].cloneNode(false)); + }); + }, + wrapAll: function(newContent) { + if (this[0]) { + $(this[0]).before(newContent = $(newContent)); + newContent.append(this); + } + return this; + }, + unwrap: function(){ + this.parent().each(function(){ + $(this).replaceWith($(this).children()); + }); + return this; + }, + hide: function(){ + return this.css("display", "none") + }, + toggle: function(setting){ + return (setting === undefined ? this.css("display") == "none" : setting) ? this.show() : this.hide(); + }, + prev: function(){ return $(this.pluck('previousElementSibling')) }, + next: function(){ return $(this.pluck('nextElementSibling')) }, + html: function(html){ + return html === undefined ? + (this.length > 0 ? this[0].innerHTML : null) : + this.each(function (idx) { + var originHtml = this.innerHTML; + $(this).empty().append( funcArg(this, html, idx, originHtml) ); + }); + }, + text: function(text){ + return text === undefined ? + (this.length > 0 ? this[0].textContent : null) : + this.each(function(){ this.textContent = text }); + }, + attr: function(name, value){ + var res; + return (typeof name == 'string' && value === undefined) ? + (this.length == 0 ? undefined : + (name == 'value' && this[0].nodeName == 'INPUT') ? this.val() : + (!(res = this[0].getAttribute(name)) && name in this[0]) ? this[0][name] : res + ) : + this.each(function(idx){ + if (isO(name)) for (key in name) this.setAttribute(key, name[key]) + else this.setAttribute(name, funcArg(this, value, idx, this.getAttribute(name))); + }); + }, + removeAttr: function(name) { + return this.each(function() { this.removeAttribute(name); }); + }, + data: function(name, value){ + return this.attr('data-' + name, value); + }, + val: function(value){ + return (value === undefined) ? + (this.length > 0 ? this[0].value : null) : + this.each(function(idx){ + this.value = funcArg(this, value, idx, this.value); + }); + }, + offset: function(){ + if(this.length==0) return null; + var obj = this[0].getBoundingClientRect(); + return { + left: obj.left + window.pageXOffset, + top: obj.top + window.pageYOffset, + width: obj.width, + height: obj.height + }; + }, + css: function(property, value){ + if (value === undefined && typeof property == 'string') { + return( + this.length == 0 + ? undefined + : this[0].style[camelize(property)] || getComputedStyle(this[0], '').getPropertyValue(property) + ); + } + var css = ''; + for (key in property) css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'; + if (typeof property == 'string') css = dasherize(property) + ":" + maybeAddPx(property, value); + return this.each(function() { this.style.cssText += ';' + css }); + }, + index: function(element){ + return element ? this.indexOf($(element)[0]) : this.parent().children().indexOf(this[0]); + }, + hasClass: function(name){ + if (this.length < 1) return false; + else return classRE(name).test(this[0].className); + }, + addClass: function(name){ + return this.each(function(idx) { + classList = []; + var cls = this.className, newName = funcArg(this, name, idx, cls); + newName.split(/\s+/g).forEach(function(klass) { + if (!$(this).hasClass(klass)) { + classList.push(klass) + } + }, this); + classList.length && (this.className += (cls ? " " : "") + classList.join(" ")) + }); + }, + removeClass: function(name){ + return this.each(function(idx) { + if(name === undefined) + return this.className = ''; + classList = this.className; + funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass) { + classList = classList.replace(classRE(klass), " ") + }); + this.className = classList.trim() + }); + }, + toggleClass: function(name, when){ + return this.each(function(idx){ + var newName = funcArg(this, name, idx, this.className); + (when === undefined ? !$(this).hasClass(newName) : when) ? + $(this).addClass(newName) : $(this).removeClass(newName); + }); + } + }; + + 'filter,add,not,eq,first,last,find,closest,parents,parent,children,siblings'.split(',').forEach(function(property){ + var fn = $.fn[property]; + $.fn[property] = function() { + var ret = fn.apply(this, arguments); + ret.prevObject = this; + return ret; + } + }); + + ['width', 'height'].forEach(function(dimension){ + $.fn[dimension] = function(value) { + var offset, Dimension = dimension.replace(/./, function(m) { return m[0].toUpperCase() }); + if (value === undefined) return this[0] == window ? window['inner' + Dimension] : + this[0] == document ? document.documentElement['offset' + Dimension] : + (offset = this.offset()) && offset[dimension]; + else return this.each(function(idx){ + var el = $(this); + el.css(dimension, funcArg(this, value, idx, el[dimension]())); + }); + } + }); + + function insert(operator, target, node) { + var parent = (operator % 2) ? target : target.parentNode; + parent && parent.insertBefore(node, + !operator ? target.nextSibling : // after + operator == 1 ? parent.firstChild : // prepend + operator == 2 ? target : // before + null); // append + } + + function traverseNode (node, fun) { + fun(node); + for (var key in node.childNodes) { + traverseNode(node.childNodes[key], fun); + } + } + + adjacencyOperators.forEach(function(key, operator) { + $.fn[key] = function(html){ + var nodes = isO(html) ? html : fragment(html); + if (!('length' in nodes) || nodes.nodeType) nodes = [nodes]; + if (nodes.length < 1) return this; + var size = this.length, copyByClone = size > 1, inReverse = operator < 2; + + return this.each(function(index, target){ + for (var i = 0; i < nodes.length; i++) { + var node = nodes[inReverse ? nodes.length-i-1 : i]; + traverseNode(node, function (node) { + if (node.nodeName != null && node.nodeName.toUpperCase() === 'SCRIPT' && (!node.type || node.type === 'text/javascript')) { + window['eval'].call(window, node.innerHTML); + } + }); + if (copyByClone && index < size - 1) node = node.cloneNode(true); + insert(operator, target, node); + } + }); + }; + + var reverseKey = (operator % 2) ? key+'To' : 'insert'+(operator ? 'Before' : 'After'); + $.fn[reverseKey] = function(html) { + $(html)[key](this); + return this; + }; + }); + + Z.prototype = $.fn; + + return $; +})(); + +window.Zepto = Zepto; +'$' in window || (window.$ = Zepto); +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. + +(function($){ + var $$ = $.qsa, handlers = {}, _zid = 1, specialEvents={}; + + specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'; + + function zid(element) { + return element._zid || (element._zid = _zid++); + } + function findHandlers(element, event, fn, selector) { + event = parse(event); + if (event.ns) var matcher = matcherFor(event.ns); + return (handlers[zid(element)] || []).filter(function(handler) { + return handler + && (!event.e || handler.e == event.e) + && (!event.ns || matcher.test(handler.ns)) + && (!fn || handler.fn == fn) + && (!selector || handler.sel == selector); + }); + } + function parse(event) { + var parts = ('' + event).split('.'); + return {e: parts[0], ns: parts.slice(1).sort().join(' ')}; + } + function matcherFor(ns) { + return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)'); + } + + function eachEvent(events, fn, iterator){ + if ($.isObject(events)) $.each(events, iterator); + else events.split(/\s/).forEach(function(type){ iterator(type, fn) }); + } + + function add(element, events, fn, selector, getDelegate){ + var id = zid(element), set = (handlers[id] || (handlers[id] = [])); + eachEvent(events, fn, function(event, fn){ + var delegate = getDelegate && getDelegate(fn, event), + callback = delegate || fn; + var proxyfn = function (event) { + var result = callback.apply(element, [event].concat(event.data)); + if (result === false) event.preventDefault(); + return result; + }; + var handler = $.extend(parse(event), {fn: fn, proxy: proxyfn, sel: selector, del: delegate, i: set.length}); + set.push(handler); + element.addEventListener(handler.e, proxyfn, false); + }); + } + function remove(element, events, fn, selector){ + var id = zid(element); + eachEvent(events || '', fn, function(event, fn){ + findHandlers(element, event, fn, selector).forEach(function(handler){ + delete handlers[id][handler.i]; + element.removeEventListener(handler.e, handler.proxy, false); + }); + }); + } + + $.event = { add: add, remove: remove } + + $.fn.bind = function(event, callback){ + return this.each(function(){ + add(this, event, callback); + }); + }; + $.fn.unbind = function(event, callback){ + return this.each(function(){ + remove(this, event, callback); + }); + }; + $.fn.one = function(event, callback){ + return this.each(function(i, element){ + add(this, event, callback, null, function(fn, type){ + return function(){ + var result = fn.apply(element, arguments); + remove(element, type, fn); + return result; + } + }); + }); + }; + + var returnTrue = function(){return true}, + returnFalse = function(){return false}, + eventMethods = { + preventDefault: 'isDefaultPrevented', + stopImmediatePropagation: 'isImmediatePropagationStopped', + stopPropagation: 'isPropagationStopped' + }; + function createProxy(event) { + var proxy = $.extend({originalEvent: event}, event); + $.each(eventMethods, function(name, predicate) { + proxy[name] = function(){ + this[predicate] = returnTrue; + return event[name].apply(event, arguments); + }; + proxy[predicate] = returnFalse; + }) + return proxy; + } + + // emulates the 'defaultPrevented' property for browsers that have none + function fix(event) { + if (!('defaultPrevented' in event)) { + event.defaultPrevented = false; + var prevent = event.preventDefault; + event.preventDefault = function() { + this.defaultPrevented = true; + prevent.call(this); + } + } + } + + $.fn.delegate = function(selector, event, callback){ + return this.each(function(i, element){ + add(element, event, callback, selector, function(fn){ + return function(e){ + var evt, match = $(e.target).closest(selector, element).get(0); + if (match) { + evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}); + return fn.apply(match, [evt].concat([].slice.call(arguments, 1))); + } + } + }); + }); + }; + $.fn.undelegate = function(selector, event, callback){ + return this.each(function(){ + remove(this, event, callback, selector); + }); + } + + $.fn.live = function(event, callback){ + $(document.body).delegate(this.selector, event, callback); + return this; + }; + $.fn.die = function(event, callback){ + $(document.body).undelegate(this.selector, event, callback); + return this; + }; + + $.fn.on = function(event, selector, callback){ + return selector === undefined || $.isFunction(selector) ? + this.bind(event, selector) : this.delegate(selector, event, callback); + }; + $.fn.off = function(event, selector, callback){ + return selector === undefined || $.isFunction(selector) ? + this.unbind(event, selector) : this.undelegate(selector, event, callback); + }; + + $.fn.trigger = function(event, data){ + if (typeof event == 'string') event = $.Event(event); + fix(event); + event.data = data; + return this.each(function(){ this.dispatchEvent(event) }); + }; + + // triggers event handlers on current element just as if an event occurred, + // doesn't trigger an actual event, doesn't bubble + $.fn.triggerHandler = function(event, data){ + var e, result; + this.each(function(i, element){ + e = createProxy(typeof event == 'string' ? $.Event(event) : event); + e.data = data; e.target = element; + $.each(findHandlers(element, event.type || event), function(i, handler){ + result = handler.proxy(e); + if (e.isImmediatePropagationStopped()) return false; + }); + }); + return result; + }; + + // shortcut methods for `.bind(event, fn)` for each event type + ('focusin focusout load resize scroll unload click dblclick '+ + 'mousedown mouseup mousemove mouseover mouseout '+ + 'change select keydown keypress keyup error').split(' ').forEach(function(event) { + $.fn[event] = function(callback){ return this.bind(event, callback) }; + }); + + ['focus', 'blur'].forEach(function(name) { + $.fn[name] = function(callback) { + if (callback) this.bind(name, callback); + else if (this.length) try { this.get(0)[name]() } catch(e){}; + return this; + }; + }); + + $.Event = function(type, props) { + var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true; + if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]); + event.initEvent(type, bubbles, true, null, null, null, null, null, null, null, null, null, null, null, null); + return event; + }; + +})(Zepto); +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. + +(function($){ + function detect(ua){ + var os = (this.os = {}), browser = (this.browser = {}), + webkit = ua.match(/WebKit\/([\d.]+)/), + android = ua.match(/(Android)\s+([\d.]+)/), + ipad = ua.match(/(iPad).*OS\s([\d_]+)/), + iphone = !ipad && ua.match(/(iPhone\sOS)\s([\d_]+)/), + webos = ua.match(/(webOS|hpwOS)[\s\/]([\d.]+)/), + touchpad = webos && ua.match(/TouchPad/), + blackberry = ua.match(/(BlackBerry).*Version\/([\d.]+)/); + + if (webkit) browser.version = webkit[1]; + browser.webkit = !!webkit; + + if (android) os.android = true, os.version = android[2]; + if (iphone) os.ios = true, os.version = iphone[2].replace(/_/g, '.'), os.iphone = true; + if (ipad) os.ios = true, os.version = ipad[2].replace(/_/g, '.'), os.ipad = true; + if (webos) os.webos = true, os.version = webos[2]; + if (touchpad) os.touchpad = true; + if (blackberry) os.blackberry = true, os.version = blackberry[2]; + } + + // ### $.os + // + // Object containing information about browser platform + // + // *Example:* + // + // $.os.ios // => true if running on Apple iOS + // $.os.android // => true if running on Android + // $.os.webos // => true if running on HP/Palm WebOS + // $.os.touchpad // => true if running on a HP TouchPad + // $.os.version // => string with a version number, e.g. + // "4.0", "3.1.1", "2.1", etc. + // $.os.iphone // => true if running on iPhone + // $.os.ipad // => true if running on iPad + // $.os.blackberry // => true if running on BlackBerry + // + // ### $.browser + // + // *Example:* + // + // $.browser.webkit // => true if the browser is WebKit-based + // $.browser.version // => WebKit version string + detect.call($, navigator.userAgent); + + // make available to unit tests + $.__detect = detect; + +})(Zepto); +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. + +(function($, undefined){ + var prefix = '', eventPrefix, endEventName, endAnimationName, + vendors = {Webkit: 'webkit', Moz: '', O: 'o', ms: 'MS'}, + document = window.document, testEl = document.createElement('div'), + supportedTransforms = /^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i; + + function downcase(str) { return str.toLowerCase() } + function normalizeEvent(name) { return eventPrefix ? eventPrefix + name : downcase(name) }; + + $.each(vendors, function(vendor, event){ + if (testEl.style[vendor + 'TransitionProperty'] !== undefined) { + prefix = '-' + downcase(vendor) + '-'; + eventPrefix = event; + return false; + } + }); + + $.fx = { + off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined), + cssPrefix: prefix, + transitionEnd: normalizeEvent('TransitionEnd'), + animationEnd: normalizeEvent('AnimationEnd') + }; + + $.fn.animate = function(properties, duration, ease, callback){ + if ($.isObject(duration)) + ease = duration.easing, callback = duration.complete, duration = duration.duration; + if (duration) duration = duration / 1000; + return this.anim(properties, duration, ease, callback); + }; + + $.fn.anim = function(properties, duration, ease, callback){ + var transforms, cssProperties = {}, key, that = this, wrappedCallback, endEvent = $.fx.transitionEnd; + if (duration === undefined) duration = 0.4; + if ($.fx.off) duration = 0; + + if (typeof properties == 'string') { + // keyframe animation + cssProperties[prefix + 'animation-name'] = properties; + cssProperties[prefix + 'animation-duration'] = duration + 's'; + endEvent = $.fx.animationEnd; + } else { + // CSS transitions + for (key in properties) + if (supportedTransforms.test(key)) { + transforms || (transforms = []); + transforms.push(key + '(' + properties[key] + ')'); + } + else cssProperties[key] = properties[key]; + + if (transforms) cssProperties[prefix + 'transform'] = transforms.join(' '); + if (!$.fx.off) cssProperties[prefix + 'transition'] = 'all ' + duration + 's ' + (ease || ''); + } + + wrappedCallback = function(){ + var props = {}; + props[prefix + 'transition'] = props[prefix + 'animation-name'] = 'none'; + $(this).css(props); + callback && callback.call(this); + } + if (duration > 0) this.one(endEvent, wrappedCallback); + + setTimeout(function() { + that.css(cssProperties); + if (duration <= 0) setTimeout(function() { + that.each(function(){ wrappedCallback.call(this) }); + }, 0); + }, 0); + + return this; + }; + + testEl = null; +})(Zepto); +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. + +(function($){ + var jsonpID = 0, + isObject = $.isObject, + document = window.document, + key, + name; + + // trigger a custom event and return false if it was cancelled + function triggerAndReturn(context, eventName, data) { + var event = $.Event(eventName); + $(context).trigger(event, data); + return !event.defaultPrevented; + } + + // trigger an Ajax "global" event + function triggerGlobal(settings, context, eventName, data) { + if (settings.global) return triggerAndReturn(context || document, eventName, data); + } + + // Number of active Ajax requests + $.active = 0; + + function ajaxStart(settings) { + if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart'); + } + function ajaxStop(settings) { + if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop'); + } + + // triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable + function ajaxBeforeSend(xhr, settings) { + var context = settings.context; + if (settings.beforeSend.call(context, xhr, settings) === false || + triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false) + return false; + + triggerGlobal(settings, context, 'ajaxSend', [xhr, settings]); + } + function ajaxSuccess(data, xhr, settings) { + var context = settings.context, status = 'success'; + settings.success.call(context, data, status, xhr); + triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data]); + ajaxComplete(status, xhr, settings); + } + // type: "timeout", "error", "abort", "parsererror" + function ajaxError(error, type, xhr, settings) { + var context = settings.context; + settings.error.call(context, xhr, type, error); + triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error]); + ajaxComplete(type, xhr, settings); + } + // status: "success", "notmodified", "error", "timeout", "abort", "parsererror" + function ajaxComplete(status, xhr, settings) { + var context = settings.context; + settings.complete.call(context, xhr, status); + triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings]); + ajaxStop(settings); + } + + // Empty function, used as default callback + function empty() {} + + // ### $.ajaxJSONP + // + // Load JSON from a server in a different domain (JSONP) + // + // *Arguments:* + // + // options — object that configure the request, + // see avaliable options below + // + // *Avaliable options:* + // + // url — url to which the request is sent + // success — callback that is executed if the request succeeds + // error — callback that is executed if the server drops error + // context — in which context to execute the callbacks in + // + // *Example:* + // + // $.ajaxJSONP({ + // url: 'http://example.com/projects?callback=?', + // success: function (data) { + // projects.push(json); + // } + // }); + // + $.ajaxJSONP = function(options){ + var callbackName = 'jsonp' + (++jsonpID), + script = document.createElement('script'), + abort = function(){ + $(script).remove(); + if (callbackName in window) window[callbackName] = empty; + ajaxComplete(xhr, options, 'abort'); + }, + xhr = { abort: abort }, abortTimeout; + + window[callbackName] = function(data){ + clearTimeout(abortTimeout); + $(script).remove(); + delete window[callbackName]; + ajaxSuccess(data, xhr, options); + }; + + script.src = options.url.replace(/=\?/, '=' + callbackName); + $('head').append(script); + + if (options.timeout > 0) abortTimeout = setTimeout(function(){ + xhr.abort(); + ajaxComplete(xhr, options, 'timeout'); + }, options.timeout); + + return xhr; + }; + + // ### $.ajaxSettings + // + // AJAX settings + // + $.ajaxSettings = { + // Default type of request + type: 'GET', + // Callback that is executed before request + beforeSend: empty, + // Callback that is executed if the request succeeds + success: empty, + // Callback that is executed the the server drops error + error: empty, + // Callback that is executed on request complete (both: error and success) + complete: empty, + // The context for the callbacks + context: null, + // Whether to trigger "global" Ajax events + global: true, + // Transport + xhr: function () { + return new window.XMLHttpRequest(); + }, + // MIME types mapping + accepts: { + script: 'text/javascript, application/javascript', + json: 'application/json', + xml: 'application/xml, text/xml', + html: 'text/html', + text: 'text/plain' + }, + // Whether the request is to another domain + crossDomain: false, + // Default timeout + timeout: 0 + }; + + // ### $.ajax + // + // Perform AJAX request + // + // *Arguments:* + // + // options — object that configure the request, + // see avaliable options below + // + // *Avaliable options:* + // + // type ('GET') — type of request GET / POST + // url (window.location) — url to which the request is sent + // data — data to send to server, + // can be string or object + // dataType ('json') — what response type you accept from + // the server: + // 'json', 'xml', 'html', or 'text' + // timeout (0) — request timeout + // beforeSend — callback that is executed before + // request send + // complete — callback that is executed on request + // complete (both: error and success) + // success — callback that is executed if + // the request succeeds + // error — callback that is executed if + // the server drops error + // context — in which context to execute the + // callbacks in + // + // *Example:* + // + // $.ajax({ + // type: 'POST', + // url: '/projects', + // data: { name: 'Zepto.js' }, + // dataType: 'html', + // timeout: 100, + // context: $('body'), + // success: function (data) { + // this.append(data); + // }, + // error: function (xhr, type) { + // alert('Error!'); + // } + // }); + // + $.ajax = function(options){ + var settings = $.extend({}, options || {}); + for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]; + + ajaxStart(settings); + + if (!settings.crossDomain) settings.crossDomain = /^([\w-]+:)?\/\/([^\/]+)/.test(settings.url) && + RegExp.$2 != window.location.host; + + if (/=\?/.test(settings.url)) return $.ajaxJSONP(settings); + + if (!settings.url) settings.url = window.location.toString(); + if (settings.data && !settings.contentType) settings.contentType = 'application/x-www-form-urlencoded'; + if (isObject(settings.data)) settings.data = $.param(settings.data); + + if (settings.type.match(/get/i) && settings.data) { + var queryString = settings.data; + if (settings.url.match(/\?.*=/)) { + queryString = '&' + queryString; + } else if (queryString[0] != '?') { + queryString = '?' + queryString; + } + settings.url += queryString; + } + + var mime = settings.accepts[settings.dataType], + baseHeaders = { }, + protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol, + xhr = $.ajaxSettings.xhr(), abortTimeout; + + if (!settings.crossDomain) baseHeaders['X-Requested-With'] = 'XMLHttpRequest'; + if (mime) baseHeaders['Accept'] = mime; + settings.headers = $.extend(baseHeaders, settings.headers || {}); + + xhr.onreadystatechange = function(){ + if (xhr.readyState == 4) { + clearTimeout(abortTimeout); + var result, error = false; + if ((xhr.status >= 200 && xhr.status < 300) || (xhr.status == 0 && protocol == 'file:')) { + if (mime == 'application/json' && !(/^\s*$/.test(xhr.responseText))) { + try { result = JSON.parse(xhr.responseText); } + catch (e) { error = e; } + } + else result = xhr.responseText; + if (error) ajaxError(error, 'parsererror', xhr, settings); + else ajaxSuccess(result, xhr, settings); + } else { + ajaxError(null, 'error', xhr, settings); + } + } + }; + + xhr.open(settings.type, settings.url, true); + + if (settings.contentType) settings.headers['Content-Type'] = settings.contentType; + for (name in settings.headers) xhr.setRequestHeader(name, settings.headers[name]); + + if (ajaxBeforeSend(xhr, settings) === false) { + xhr.abort(); + return false; + } + + if (settings.timeout > 0) abortTimeout = setTimeout(function(){ + xhr.onreadystatechange = empty; + xhr.abort(); + ajaxError(null, 'timeout', xhr, settings); + }, settings.timeout); + + xhr.send(settings.data); + return xhr; + }; + + // ### $.get + // + // Load data from the server using a GET request + // + // *Arguments:* + // + // url — url to which the request is sent + // success — callback that is executed if the request succeeds + // + // *Example:* + // + // $.get( + // '/projects/42', + // function (data) { + // $('body').append(data); + // } + // ); + // + $.get = function(url, success){ return $.ajax({ url: url, success: success }) }; + + // ### $.post + // + // Load data from the server using POST request + // + // *Arguments:* + // + // url — url to which the request is sent + // [data] — data to send to server, can be string or object + // [success] — callback that is executed if the request succeeds + // [dataType] — type of expected response + // 'json', 'xml', 'html', or 'text' + // + // *Example:* + // + // $.post( + // '/projects', + // { name: 'Zepto.js' }, + // function (data) { + // $('body').append(data); + // }, + // 'html' + // ); + // + $.post = function(url, data, success, dataType){ + if ($.isFunction(data)) dataType = dataType || success, success = data, data = null; + return $.ajax({ type: 'POST', url: url, data: data, success: success, dataType: dataType }); + }; + + // ### $.getJSON + // + // Load JSON from the server using GET request + // + // *Arguments:* + // + // url — url to which the request is sent + // success — callback that is executed if the request succeeds + // + // *Example:* + // + // $.getJSON( + // '/projects/42', + // function (json) { + // projects.push(json); + // } + // ); + // + $.getJSON = function(url, success){ + return $.ajax({ url: url, success: success, dataType: 'json' }); + }; + + // ### $.fn.load + // + // Load data from the server into an element + // + // *Arguments:* + // + // url — url to which the request is sent + // [success] — callback that is executed if the request succeeds + // + // *Examples:* + // + // $('#project_container').get( + // '/projects/42', + // function () { + // alert('Project was successfully loaded'); + // } + // ); + // + // $('#project_comments').get( + // '/projects/42 #comments', + // function () { + // alert('Comments was successfully loaded'); + // } + // ); + // + $.fn.load = function(url, success){ + if (!this.length) return this; + var self = this, parts = url.split(/\s/), selector; + if (parts.length > 1) url = parts[0], selector = parts[1]; + $.get(url, function(response){ + self.html(selector ? + $(document.createElement('div')).html(response).find(selector).html() + : response); + success && success.call(self); + }); + return this; + }; + + var escape = encodeURIComponent; + + function serialize(params, obj, traditional, scope){ + var array = $.isArray(obj); + $.each(obj, function(key, value) { + if (scope) key = traditional ? scope : scope + '[' + (array ? '' : key) + ']'; + // handle data in serializeArray() format + if (!scope && array) params.add(value.name, value.value); + // recurse into nested objects + else if (traditional ? $.isArray(value) : isObject(value)) + serialize(params, value, traditional, key); + else params.add(key, value); + }); + } + + // ### $.param + // + // Encode object as a string of URL-encoded key-value pairs + // + // *Arguments:* + // + // obj — object to serialize + // [traditional] — perform shallow serialization + // + // *Example:* + // + // $.param( { name: 'Zepto.js', version: '0.6' } ); + // + $.param = function(obj, traditional){ + var params = []; + params.add = function(k, v){ this.push(escape(k) + '=' + escape(v)) }; + serialize(params, obj, traditional); + return params.join('&').replace('%20', '+'); + }; +})(Zepto); +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. + +(function ($) { + + // ### $.fn.serializeArray + // + // Encode a set of form elements as an array of names and values + // + // *Example:* + // + // $('#login_form').serializeArray(); + // + // returns + // + // [ + // { + // name: 'email', + // value: 'koss@nocorp.me' + // }, + // { + // name: 'password', + // value: '123456' + // } + // ] + // + $.fn.serializeArray = function () { + var result = [], el; + $( Array.prototype.slice.call(this.get(0).elements) ).each(function () { + el = $(this); + var type = el.attr('type'); + if ( + !this.disabled && type != 'submit' && type != 'reset' && type != 'button' && + ((type != 'radio' && type != 'checkbox') || this.checked) + ) { + result.push({ + name: el.attr('name'), + value: el.val() + }); + } + }); + return result; + }; + + // ### $.fn.serialize + // + // + // Encode a set of form elements as a string for submission + // + // *Example:* + // + // $('#login_form').serialize(); + // + // returns + // + // "email=koss%40nocorp.me&password=123456" + // + $.fn.serialize = function () { + var result = []; + this.serializeArray().forEach(function (elm) { + result.push( encodeURIComponent(elm.name) + '=' + encodeURIComponent(elm.value) ); + }); + return result.join('&'); + }; + + // ### $.fn.submit + // + // Bind or trigger the submit event for a form + // + // *Examples:* + // + // To bind a handler for the submit event: + // + // $('#login_form').submit(function (e) { + // alert('Form was submitted!'); + // e.preventDefault(); + // }); + // + // To trigger form submit: + // + // $('#login_form').submit(); + // + $.fn.submit = function (callback) { + if (callback) this.bind('submit', callback) + else if (this.length) { + var event = $.Event('submit'); + this.eq(0).trigger(event); + if (!event.defaultPrevented) this.get(0).submit() + } + return this; + } + +})(Zepto); +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. + +(function($){ + var touch = {}, touchTimeout; + + function parentIfText(node){ + return 'tagName' in node ? node : node.parentNode; + } + + function swipeDirection(x1, x2, y1, y2){ + var xDelta = Math.abs(x1 - x2), yDelta = Math.abs(y1 - y2); + if (xDelta >= yDelta) { + return (x1 - x2 > 0 ? 'Left' : 'Right'); + } else { + return (y1 - y2 > 0 ? 'Up' : 'Down'); + } + } + + var longTapDelay = 750; + function longTap(){ + if (touch.last && (Date.now() - touch.last >= longTapDelay)) { + $(touch.target).trigger('longTap'); + touch = {}; + } + } + + $(document).ready(function(){ + $(document.body).bind('touchstart', function(e){ + var now = Date.now(), delta = now - (touch.last || now); + touch.target = parentIfText(e.touches[0].target); + touchTimeout && clearTimeout(touchTimeout); + touch.x1 = e.touches[0].pageX; + touch.y1 = e.touches[0].pageY; + if (delta > 0 && delta <= 250) touch.isDoubleTap = true; + touch.last = now; + setTimeout(longTap, longTapDelay); + }).bind('touchmove', function(e){ + touch.x2 = e.touches[0].pageX; + touch.y2 = e.touches[0].pageY; + }).bind('touchend', function(e){ + if (touch.isDoubleTap) { + $(touch.target).trigger('doubleTap'); + touch = {}; + } else if (touch.x2 > 0 || touch.y2 > 0) { + (Math.abs(touch.x1 - touch.x2) > 30 || Math.abs(touch.y1 - touch.y2) > 30) && + $(touch.target).trigger('swipe') && + $(touch.target).trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2))); + touch.x1 = touch.x2 = touch.y1 = touch.y2 = touch.last = 0; + } else if ('last' in touch) { + touchTimeout = setTimeout(function(){ + touchTimeout = null; + $(touch.target).trigger('tap') + touch = {}; + }, 250); + } + }).bind('touchcancel', function(){ touch = {} }); + }); + + ['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'longTap'].forEach(function(m){ + $.fn[m] = function(callback){ return this.bind(m, callback) } + }); +})(Zepto); diff --git a/static/js/libs/zepto.min.js b/static/js/libs/zepto.min.js new file mode 100644 index 0000000..2d07776 --- /dev/null +++ b/static/js/libs/zepto.min.js @@ -0,0 +1,4 @@ +// Zepto.js +// (c) 2010, 2011 Thomas Fuchs +// Zepto.js may be freely distributed under the MIT license. +(function(a){String.prototype.trim===a&&(String.prototype.trim=function(){return this.replace(/^\s+/,"").replace(/\s+$/,"")}),Array.prototype.reduce===a&&(Array.prototype.reduce=function(b){if(this===void 0||this===null)throw new TypeError;var c=Object(this),d=c.length>>>0,e=0,f;if(typeof b!="function")throw new TypeError;if(d==0&&arguments.length==1)throw new TypeError;if(arguments.length>=2)f=arguments[1];else do{if(e in c){f=c[e++];break}if(++e>=d)throw new TypeError}while(!0);while(e0?[].concat.apply([],a):a}function B(a){return a.replace(/-+(.)?/g,function(a,b){return b?b.toUpperCase():""})}function C(a){return a.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function D(a){return a.filter(function(a,b,c){return c.indexOf(a)==b})}function E(a){return a in i?i[a]:i[a]=new RegExp("(^|\\s)"+a+"(\\s|$)")}function F(a,b){return typeof b=="number"&&!k[C(a)]?b+"px":b}function G(a){var b,c;return h[a]||(b=g.createElement(a),g.body.appendChild(b),c=j(b,"").getPropertyValue("display"),b.parentNode.removeChild(b),c=="none"&&(c="block"),h[a]=c),h[a]}function H(b,c){c===a&&l.test(b)&&RegExp.$1,c in q||(c="*");var d=q[c];return d.innerHTML=""+b,f.call(d.childNodes)}function I(a,b){return a=a||e,a.__proto__=I.prototype,a.selector=b||"",a}function J(b,d){if(!b)return I();if(d!==a)return J(d).find(b);if(v(b))return J(g).ready(b);if(b instanceof I)return b;var e;return x(b)?e=z(b):m.indexOf(b.nodeType)>=0||b===window?(e=[b],b=null):l.test(b)?(e=H(b.trim(),RegExp.$1),b=null):b.nodeType&&b.nodeType==3?e=[b]:e=c(g,b),I(e,b)}function K(b,c){return c===a?J(b):J(b).filter(c)}function L(a,b,c,d){return v(b)?b.call(a,c,d):b}function M(a,b,c){var d=a%2?b:b.parentNode;d&&d.insertBefore(c,a?a==1?d.firstChild:a==2?b:null:b.nextSibling)}function N(a,b){b(a);for(var c in a.childNodes)N(a.childNodes[c],b)}var a,b,c,d,e=[],f=e.slice,g=window.document,h={},i={},j=g.defaultView.getComputedStyle,k={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},l=/^\s*<(\w+)[^>]*>/,m=[1,9,11],n=["after","prepend","before","append"],o=g.createElement("table"),p=g.createElement("tr"),q={tr:g.createElement("tbody"),tbody:o,thead:o,tfoot:o,td:p,th:p,"*":g.createElement("div")},r=/complete|loaded|interactive/,s=/^\.([\w-]+)$/,t=/^#([\w-]+)$/,u=/^[\w-]+$/;return J.extend=function(a){return f.call(arguments,1).forEach(function(c){for(b in c)a[b]=c[b]}),a},J.qsa=c=function(a,b){var c;return a===g&&t.test(b)?(c=a.getElementById(RegExp.$1))?[c]:e:f.call(s.test(b)?a.getElementsByClassName(RegExp.$1):u.test(b)?a.getElementsByTagName(b):a.querySelectorAll(b))},J.isFunction=v,J.isObject=w,J.isArray=x,J.map=function(a,b){var c,d=[],e,f;if(y(a))for(e=0;e=0}))},end:function(){return this.prevObject||J()},andSelf:function(){return this.add(this.prevObject||J())},add:function(a,b){return J(D(this.concat(J(a,b))))},is:function(a){return this.length>0&&J(this[0]).filter(a).length>0},not:function(b){var c=[];if(v(b)&&b.call!==a)this.each(function(a){b.call(this,a)||c.push(this)});else{var d=typeof b=="string"?this.filter(b):y(b)&&v(b.item)?f.call(b):J(b);this.forEach(function(a){d.indexOf(a)<0&&c.push(a)})}return J(c)},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){var a=this[0];return a&&!w(a)?a:J(a)},last:function(){var a=this[this.length-1];return a&&!w(a)?a:J(a)},find:function(a){var b;return this.length==1?b=c(this[0],a):b=this.map(function(){return c(this,a)}),J(b)},closest:function(a,b){var d=this[0],e=c(b||g,a);e.length||(d=null);while(d&&e.indexOf(d)<0)d=d!==b&&d!==g&&d.parentNode;return J(d)},parents:function(a){var b=[],c=this;while(c.length>0)c=J.map(c,function(a){if((a=a.parentNode)&&a!==g&&b.indexOf(a)<0)return b.push(a),a});return K(b,a)},parent:function(a){return K(D(this.pluck("parentNode")),a)},children:function(a){return K(this.map(function(){return f.call(this.children)}),a)},siblings:function(a){return K(this.map(function(a,b){return f.call(b.parentNode.children).filter(function(a){return a!==b})}),a)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(a){return this.map(function(){return this[a]})},show:function(){return this.each(function(){this.style.display=="none"&&(this.style.display=null),j(this,"").getPropertyValue("display")=="none"&&(this.style.display=G(this.nodeName))})},replaceWith:function(a){return this.each(function(){J(this).before(a).remove()})},wrap:function(a){return this.each(function(){J(this).wrapAll(J(a)[0].cloneNode(!1))})},wrapAll:function(a){return this[0]&&(J(this[0]).before(a=J(a)),a.append(this)),this},unwrap:function(){return this.parent().each(function(){J(this).replaceWith(J(this).children())}),this},hide:function(){return this.css("display","none")},toggle:function(b){return(b===a?this.css("display")=="none":b)?this.show():this.hide()},prev:function(){return J(this.pluck("previousElementSibling"))},next:function(){return J(this.pluck("nextElementSibling"))},html:function(b){return b===a?this.length>0?this[0].innerHTML:null:this.each(function(a){var c=this.innerHTML;J(this).empty().append(L(this,b,a,c))})},text:function(b){return b===a?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=b})},attr:function(c,d){var e;return typeof c=="string"&&d===a?this.length==0?a:c=="value"&&this[0].nodeName=="INPUT"?this.val():!(e=this[0].getAttribute(c))&&c in this[0]?this[0][c]:e:this.each(function(a){if(w(c))for(b in c)this.setAttribute(b,c[b]);else this.setAttribute(c,L(this,d,a,this.getAttribute(c)))})},removeAttr:function(a){return this.each(function(){this.removeAttribute(a)})},data:function(a,b){return this.attr("data-"+a,b)},val:function(b){return b===a?this.length>0?this[0].value:null:this.each(function(a){this.value=L(this,b,a,this.value)})},offset:function(){if(this.length==0)return null;var a=this[0].getBoundingClientRect();return{left:a.left+window.pageXOffset,top:a.top+window.pageYOffset,width:a.width,height:a.height}},css:function(c,d){if(d===a&&typeof c=="string")return this.length==0?a:this[0].style[B(c)]||j(this[0],"").getPropertyValue(c);var e="";for(b in c)e+=C(b)+":"+F(b,c[b])+";";return typeof c=="string"&&(e=C(c)+":"+F(c,d)),this.each(function(){this.style.cssText+=";"+e})},index:function(a){return a?this.indexOf(J(a)[0]):this.parent().children().indexOf(this[0])},hasClass:function(a){return this.length<1?!1:E(a).test(this[0].className)},addClass:function(a){return this.each(function(b){d=[];var c=this.className,e=L(this,a,b,c);e.split(/\s+/g).forEach(function(a){J(this).hasClass(a)||d.push(a)},this),d.length&&(this.className+=(c?" ":"")+d.join(" "))})},removeClass:function(b){return this.each(function(c){if(b===a)return this.className="";d=this.className,L(this,b,c,d).split(/\s+/g).forEach(function(a){d=d.replace(E(a)," ")}),this.className=d.trim()})},toggleClass:function(b,c){return this.each(function(d){var e=L(this,b,d,this.className);(c===a?!J(this).hasClass(e):c)?J(this).addClass(e):J(this).removeClass(e)})}},"filter,add,not,eq,first,last,find,closest,parents,parent,children,siblings".split(",").forEach(function(a){var b=J.fn[a];J.fn[a]=function(){var a=b.apply(this,arguments);return a.prevObject=this,a}}),["width","height"].forEach(function(b){J.fn[b]=function(c){var d,e=b.replace(/./,function(a){return a[0].toUpperCase()});return c===a?this[0]==window?window["inner"+e]:this[0]==g?g.documentElement["offset"+e]:(d=this.offset())&&d[b]:this.each(function(a){var d=J(this);d.css(b,L(this,c,a,d[b]()))})}}),n.forEach(function(a,b){J.fn[a]=function(a){var c=w(a)?a:H(a);if(!("length"in c)||c.nodeType)c=[c];if(c.length<1)return this;var d=this.length,e=d>1,f=b<2;return this.each(function(a,g){for(var h=0;h0&&this.one(n,m),setTimeout(function(){l.css(i),e<=0&&setTimeout(function(){l.each(function(){m.call(this)})},0)},0),this},i=null}(Zepto),function(a){function g(b,c,d){var e=a.Event(c);return a(b).trigger(e,d),!e.defaultPrevented}function h(a,b,c,e){if(a.global)return g(b||d,c,e)}function i(b){b.global&&a.active++===0&&h(b,null,"ajaxStart")}function j(b){b.global&&!--a.active&&h(b,null,"ajaxStop")}function k(a,b){var c=b.context;if(b.beforeSend.call(c,a,b)===!1||h(b,c,"ajaxBeforeSend",[a,b])===!1)return!1;h(b,c,"ajaxSend",[a,b])}function l(a,b,c){var d=c.context,e="success";c.success.call(d,a,e,b),h(c,d,"ajaxSuccess",[b,c,a]),n(e,b,c)}function m(a,b,c,d){var e=d.context;d.error.call(e,c,b,a),h(d,e,"ajaxError",[c,d,a]),n(b,c,d)}function n(a,b,c){var d=c.context;c.complete.call(d,b,a),h(c,d,"ajaxComplete",[b,c]),j(c)}function o(){}function q(b,d,e,f){var g=a.isArray(d);a.each(d,function(d,h){f&&(d=e?f:f+"["+(g?"":d)+"]"),!f&&g?b.add(h.name,h.value):(e?a.isArray(h):c(h))?q(b,h,e,d):b.add(d,h)})}var b=0,c=a.isObject,d=window.document,e,f;a.active=0,a.ajaxJSONP=function(c){var e="jsonp"+ ++b,f=d.createElement("script"),g=function(){a(f).remove(),e in window&&(window[e]=o),n(h,c,"abort")},h={abort:g},i;return window[e]=function(b){clearTimeout(i),a(f).remove(),delete window[e],l(b,h,c)},f.src=c.url.replace(/=\?/,"="+e),a("head").append(f),c.timeout>0&&(i=setTimeout(function(){h.abort(),n(h,c,"timeout")},c.timeout)),h},a.ajaxSettings={type:"GET",beforeSend:o,success:o,error:o,complete:o,context:null,global:!0,xhr:function(){return new window.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript",json:"application/json",xml:"application/xml, text/xml",html:"text/html",text:"text/plain"},crossDomain:!1,timeout:0},a.ajax=function(b){var d=a.extend({},b||{});for(e in a.ajaxSettings)d[e]===undefined&&(d[e]=a.ajaxSettings[e]);i(d),d.crossDomain||(d.crossDomain=/^([\w-]+:)?\/\/([^\/]+)/.test(d.url)&&RegExp.$2!=window.location.host);if(/=\?/.test(d.url))return a.ajaxJSONP(d);d.url||(d.url=window.location.toString()),d.data&&!d.contentType&&(d.contentType="application/x-www-form-urlencoded"),c(d.data)&&(d.data=a.param(d.data));if(d.type.match(/get/i)&&d.data){var g=d.data;d.url.match(/\?.*=/)?g="&"+g:g[0]!="?"&&(g="?"+g),d.url+=g}var h=d.accepts[d.dataType],j={},n=/^([\w-]+:)\/\//.test(d.url)?RegExp.$1:window.location.protocol,p=a.ajaxSettings.xhr(),q;d.crossDomain||(j["X-Requested-With"]="XMLHttpRequest"),h&&(j.Accept=h),d.headers=a.extend(j,d.headers||{}),p.onreadystatechange=function(){if(p.readyState==4){clearTimeout(q);var a,b=!1;if(p.status>=200&&p.status<300||p.status==0&&n=="file:"){if(h=="application/json"&&!/^\s*$/.test(p.responseText))try{a=JSON.parse(p.responseText)}catch(c){b=c}else a=p.responseText;b?m(b,"parsererror",p,d):l(a,p,d)}else m(null,"error",p,d)}},p.open(d.type,d.url,!0),d.contentType&&(d.headers["Content-Type"]=d.contentType);for(f in d.headers)p.setRequestHeader(f,d.headers[f]);return k(p,d)===!1?(p.abort(),!1):(d.timeout>0&&(q=setTimeout(function(){p.onreadystatechange=o,p.abort(),m(null,"timeout",p,d)},d.timeout)),p.send(d.data),p)},a.get=function(b,c){return a.ajax({url:b,success:c})},a.post=function(b,c,d,e){return a.isFunction(c)&&(e=e||d,d=c,c=null),a.ajax({type:"POST",url:b,data:c,success:d,dataType:e})},a.getJSON=function(b,c){return a.ajax({url:b,success:c,dataType:"json"})},a.fn.load=function(b,c){if(!this.length)return this;var e=this,f=b.split(/\s/),g;return f.length>1&&(b=f[0],g=f[1]),a.get(b,function(b){e.html(g?a(d.createElement("div")).html(b).find(g).html():b),c&&c.call(e)}),this};var p=encodeURIComponent;a.param=function(a,b){var c=[];return c.add=function(a,b){this.push(p(a)+"="+p(b))},q(c,a,b),c.join("&").replace("%20","+")}}(Zepto),function(a){a.fn.serializeArray=function(){var b=[],c;return a(Array.prototype.slice.call(this.get(0).elements)).each(function(){c=a(this);var d=c.attr("type");!this.disabled&&d!="submit"&&d!="reset"&&d!="button"&&(d!="radio"&&d!="checkbox"||this.checked)&&b.push({name:c.attr("name"),value:c.val()})}),b},a.fn.serialize=function(){var a=[];return this.serializeArray().forEach(function(b){a.push(encodeURIComponent(b.name)+"="+encodeURIComponent(b.value))}),a.join("&")},a.fn.submit=function(b){if(b)this.bind("submit",b);else if(this.length){var c=a.Event("submit");this.eq(0).trigger(c),c.defaultPrevented||this.get(0).submit()}return this}}(Zepto),function(a){function d(a){return"tagName"in a?a:a.parentNode}function e(a,b,c,d){var e=Math.abs(a-b),f=Math.abs(c-d);return e>=f?a-b>0?"Left":"Right":c-d>0?"Up":"Down"}function g(){b.last&&Date.now()-b.last>=f&&(a(b.target).trigger("longTap"),b={})}var b={},c,f=750;a(document).ready(function(){a(document.body).bind("touchstart",function(a){var e=Date.now(),h=e-(b.last||e);b.target=d(a.touches[0].target),c&&clearTimeout(c),b.x1=a.touches[0].pageX,b.y1=a.touches[0].pageY,h>0&&h<=250&&(b.isDoubleTap=!0),b.last=e,setTimeout(g,f)}).bind("touchmove",function(a){b.x2=a.touches[0].pageX,b.y2=a.touches[0].pageY}).bind("touchend",function(d){b.isDoubleTap?(a(b.target).trigger("doubleTap"),b={}):b.x2>0||b.y2>0?((Math.abs(b.x1-b.x2)>30||Math.abs(b.y1-b.y2)>30)&&a(b.target).trigger("swipe")&&a(b.target).trigger("swipe"+e(b.x1,b.x2,b.y1,b.y2)),b.x1=b.x2=b.y1=b.y2=b.last=0):"last"in b&&(c=setTimeout(function(){c=null,a(b.target).trigger("tap"),b={}},250))}).bind("touchcancel",function(){b={}})}),["swipe","swipeLeft","swipeRight","swipeUp","swipeDown","doubleTap","tap","longTap"].forEach(function(b){a.fn[b]=function(a){return this.bind(b,a)}})}(Zepto) diff --git a/static/js/mylibs/.gitignore b/static/js/mylibs/.gitignore new file mode 100755 index 0000000..4397c3a --- /dev/null +++ b/static/js/mylibs/.gitignore @@ -0,0 +1,2 @@ +!.gitignore + diff --git a/static/js/plugins.js b/static/js/plugins.js new file mode 100755 index 0000000..8f332e8 --- /dev/null +++ b/static/js/plugins.js @@ -0,0 +1,42 @@ + +// usage: log('inside coolFunc', this, arguments); +// paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ +window.log = function(){ + log.history = log.history || []; // store logs to an array for reference + log.history.push(arguments); + if(this.console) { + arguments.callee = arguments.callee.caller; + var newarr = [].slice.call(arguments); + (typeof console.log === 'object' ? log.apply.call(console.log, console, newarr) : console.log.apply(console, newarr)); + } +}; + +// make it safe to use console.log always +(function(b){function c(){}for(var d="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,timeStamp,profile,profileEnd,time,timeEnd,trace,warn".split(","),a;a=d.pop();){b[a]=b[a]||c}})((function(){try +{console.log();return window.console;}catch(err){return window.console={};}})()); + + +// place any jQuery/helper plugins in here, instead of separate, slower script files. +/* plugins.css +part of PlainReader by Luke Hagan +created: 2011-11-05 +released under the MIT license (see LICENSE.txt for details) */ + +function print(data) { + console.log(data); +} + +// https://github.com/madrobby/zepto/issues/247 +// with modifications (.each didn't work for some reason) +$.fn.clone=function(){ + return this[0].cloneNode(true); +} + +// strip HTML tags +// http://stackoverflow.com/questions/822452/strip-html-from-text-javascript +function stripTags(html) { + var tmp = document.createElement("DIV"); + tmp.innerHTML = html; + return tmp.textContent||tmp.innerText; + delete tmp; +} diff --git a/static/js/script.js b/static/js/script.js new file mode 100755 index 0000000..d750809 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,203 @@ +/* plugins.css +part of PlainReader by Luke Hagan +created: 2011-11-05 +released under the MIT license (see LICENSE.txt for details) */ + +var unreaditems; + +$(document).ready(function(){ + function instapaperText(data) { + $('#content .body_text').html(data); + $('#content .body_text a').attr('target', '_blank'); + $('#content header a').unbind('click'); + + $('#content header a').bind('click', function(event) { + // TODO: less hacky way to do this? + var site = $('.selected .ident_site').html(); + var story = $('.selected .ident_story').html(); + var story_obj = unreaditems[story]; + $('#content .body_text').html(story_obj.story_content); + $('#content header a').unbind('click'); + $('#content header a').bind('click', function(event) { + $.get('/text?url=' + $('#content header a').attr('href'), instapaperText ); + event.preventDefault(); + }); + event.preventDefault(); + }); + } + + function getUnread(json) { + unreaditems = json.stories; + + var list_template = $('#template'); + for (var i=0; i