| 1 | """ |
|---|
| 2 | Text Helpers |
|---|
| 3 | """ |
|---|
| 4 | # Last synced with Rails copy at Revision 4595 on Aug 19th, 2006. |
|---|
| 5 | # Purposely left out sanitize, should be included at some point likely using |
|---|
| 6 | # BeautifulSoup. |
|---|
| 7 | |
|---|
| 8 | from routes import request_config |
|---|
| 9 | from webhelpers.rails.tags import content_tag, tag_options |
|---|
| 10 | import webhelpers.textile as textile |
|---|
| 11 | import webhelpers.markdown as markdown |
|---|
| 12 | import itertools, re |
|---|
| 13 | |
|---|
| 14 | AUTO_LINK_RE = re.compile("""(<\w+.*?>|[^=!:'"\/]|^)((?:http[s]?:\/\/)|(?:www\.))(([\w]+:?[=?&\/.-]?)*\w+[\/]?(?:\#\w*)?)([\.,"'?!;:]|\s|<|$)""") |
|---|
| 15 | |
|---|
| 16 | def iterdict(items): |
|---|
| 17 | return dict(items=items, iter=itertools.cycle(items)) |
|---|
| 18 | |
|---|
| 19 | def cycle(*args, **kargs): |
|---|
| 20 | """ |
|---|
| 21 | Returns the next cycle of the given list |
|---|
| 22 | |
|---|
| 23 | Everytime ``cycle`` is called, the value returned will be the next item |
|---|
| 24 | in the list passed to it. This list is reset on every request, but can |
|---|
| 25 | also be reset by calling ``reset_cycle()``. |
|---|
| 26 | |
|---|
| 27 | You may specify the list as either arguments, or as a single list argument. |
|---|
| 28 | |
|---|
| 29 | This can be used to alternate classes for table rows:: |
|---|
| 30 | |
|---|
| 31 | # In Myghty... |
|---|
| 32 | % for item in items: |
|---|
| 33 | <tr class="<% cycle("even", "odd") %>"> |
|---|
| 34 | ... use item ... |
|---|
| 35 | </tr> |
|---|
| 36 | % #endfor |
|---|
| 37 | |
|---|
| 38 | You can use named cycles to prevent clashes in nested loops. You'll |
|---|
| 39 | have to reset the inner cycle, manually:: |
|---|
| 40 | |
|---|
| 41 | % for item in items: |
|---|
| 42 | <tr class="<% cycle("even", "odd", name="row_class") %> |
|---|
| 43 | <td> |
|---|
| 44 | % for value in item.values: |
|---|
| 45 | <span style="color:'<% cycle("red", "green", "blue", |
|---|
| 46 | name="colors") %>'"> |
|---|
| 47 | item |
|---|
| 48 | </span> |
|---|
| 49 | % #endfor |
|---|
| 50 | <% reset_cycle("colors") %> |
|---|
| 51 | </td> |
|---|
| 52 | </tr> |
|---|
| 53 | % #endfor |
|---|
| 54 | """ |
|---|
| 55 | if len(args) > 1: |
|---|
| 56 | items = args |
|---|
| 57 | else: |
|---|
| 58 | items = args[0] |
|---|
| 59 | name = kargs.get('name', 'default') |
|---|
| 60 | cycles = request_config().environ.setdefault('railshelpers.cycles', {}) |
|---|
| 61 | |
|---|
| 62 | cycle = cycles.setdefault(name, iterdict(items)) |
|---|
| 63 | |
|---|
| 64 | if cycles[name].get('items') != items: |
|---|
| 65 | cycle = cycles[name] = iterdict(items) |
|---|
| 66 | return cycle['iter'].next() |
|---|
| 67 | |
|---|
| 68 | def reset_cycle(name='default'): |
|---|
| 69 | """ |
|---|
| 70 | Resets a cycle |
|---|
| 71 | |
|---|
| 72 | Resets the cycle so that it starts from the first element in the array |
|---|
| 73 | the next time it is used. |
|---|
| 74 | """ |
|---|
| 75 | del request_config().environ['railshelpers.cycles'][name] |
|---|
| 76 | |
|---|
| 77 | def truncate(text, length=30, truncate_string='...'): |
|---|
| 78 | """ |
|---|
| 79 | Truncates ``text`` with replacement characters |
|---|
| 80 | |
|---|
| 81 | ``length`` |
|---|
| 82 | The maximum length of ``text`` before replacement |
|---|
| 83 | ``truncate_string`` |
|---|
| 84 | If ``text`` exceeds the ``length``, this string will replace |
|---|
| 85 | the end of the string |
|---|
| 86 | """ |
|---|
| 87 | if not text: return '' |
|---|
| 88 | |
|---|
| 89 | new_len = length-len(truncate_string) |
|---|
| 90 | if len(text) > length: |
|---|
| 91 | return text[:new_len] + truncate_string |
|---|
| 92 | else: |
|---|
| 93 | return text |
|---|
| 94 | |
|---|
| 95 | def highlight(text, phrase, hilighter='<strong class="hilight">\\1</strong>'): |
|---|
| 96 | """ |
|---|
| 97 | Highlights the ``phrase`` where it is found in the ``text`` |
|---|
| 98 | |
|---|
| 99 | The highlighted phrase will be surrounded by the hilighter, by default:: |
|---|
| 100 | |
|---|
| 101 | <strong class="highlight">I'm a highlight phrase</strong> |
|---|
| 102 | |
|---|
| 103 | ``highlighter`` |
|---|
| 104 | Defines the highlighting phrase. This argument should be a single-quoted string |
|---|
| 105 | with ``\\1`` where the phrase is supposed to be inserted. |
|---|
| 106 | |
|---|
| 107 | Note: The ``phrase`` is sanitized to include only letters, digits, and spaces before use. |
|---|
| 108 | """ |
|---|
| 109 | if not phrase or not text: |
|---|
| 110 | return text |
|---|
| 111 | return re.sub(re.compile('(%s)' % re.escape(phrase)), hilighter, text, re.I) |
|---|
| 112 | |
|---|
| 113 | def excerpt(text, phrase, radius=100, excerpt_string="..."): |
|---|
| 114 | """ |
|---|
| 115 | Extracts an excerpt from the ``text`` |
|---|
| 116 | |
|---|
| 117 | ``phrase`` |
|---|
| 118 | Phrase to excerpt from ``text`` |
|---|
| 119 | ``radius`` |
|---|
| 120 | How many surrounding characters to include |
|---|
| 121 | ``excerpt_string`` |
|---|
| 122 | Characters surrounding entire excerpt |
|---|
| 123 | |
|---|
| 124 | Example:: |
|---|
| 125 | |
|---|
| 126 | >>> excerpt("hello my world", "my", 3) |
|---|
| 127 | "...lo my wo..." |
|---|
| 128 | """ |
|---|
| 129 | if not text or not phrase: |
|---|
| 130 | return text |
|---|
| 131 | |
|---|
| 132 | pat = re.compile('(.{0,%s}%s.{0,%s})' % (radius, re.escape(phrase), radius)) |
|---|
| 133 | match = pat.search(text) |
|---|
| 134 | if not match: |
|---|
| 135 | return "" |
|---|
| 136 | return excerpt_string + match.expand(r'\1') + excerpt_string |
|---|
| 137 | |
|---|
| 138 | def word_wrap(text, line_width=80): |
|---|
| 139 | """ |
|---|
| 140 | Word wrap long lines to ``line_width`` |
|---|
| 141 | """ |
|---|
| 142 | text = re.sub(r'\n', '\n\n', text) |
|---|
| 143 | return re.sub(r'(.{1,%s})(\s+|$)' % line_width, r'\1\n', text).strip() |
|---|
| 144 | |
|---|
| 145 | def simple_format(text): |
|---|
| 146 | """ |
|---|
| 147 | Returns ``text`` transformed into HTML using very simple formatting rules |
|---|
| 148 | |
|---|
| 149 | Surrounds paragraphs with ``<p>`` tags, and converts line breaks into ``<br />`` |
|---|
| 150 | Two consecutive newlines(``\\n\\n``) are considered as a paragraph, one newline (``\\n``) is |
|---|
| 151 | considered a linebreak, three or more consecutive newlines are turned into two newlines. |
|---|
| 152 | """ |
|---|
| 153 | text = re.sub(r'(\r\n|\n|\r)', r'\n', text) |
|---|
| 154 | text = re.sub(r'\n\n+', r'\n\n', text) |
|---|
| 155 | text = re.sub(r'(\n\n)', r'</p>\1<p>', text) |
|---|
| 156 | text = re.sub(r'([^\n])(\n)([^\n])', r'\1\2<br />\3', text) |
|---|
| 157 | text = content_tag("p", text).replace('</p><p></p>', '</p>') |
|---|
| 158 | text = re.sub(r'</p><p>', r'</p>\n<p>', text) |
|---|
| 159 | return text |
|---|
| 160 | |
|---|
| 161 | def auto_link(text, link="all", **href_options): |
|---|
| 162 | """ |
|---|
| 163 | Turns all urls and email addresses into clickable links. |
|---|
| 164 | |
|---|
| 165 | ``link`` |
|---|
| 166 | Used to determine what to link. Options are "all", "email_addresses", or "urls" |
|---|
| 167 | |
|---|
| 168 | Example:: |
|---|
| 169 | |
|---|
| 170 | >>> auto_link("Go to http://www.planetpython.com and say hello to guido@python.org") |
|---|
| 171 | 'Go to <a href="http://www.planetpython.com">http://www.planetpython.com</a> and say |
|---|
| 172 | hello to <a href="mailto:guido@python.org">guido@python.org</a>' |
|---|
| 173 | """ |
|---|
| 174 | if not text: |
|---|
| 175 | return "" |
|---|
| 176 | if link == "all": |
|---|
| 177 | return auto_link_urls(auto_link_email_addresses(text), **href_options) |
|---|
| 178 | elif link == "email_addresses": |
|---|
| 179 | return auto_link_email_addresses(text) |
|---|
| 180 | else: |
|---|
| 181 | return auto_link_urls(text, **href_options) |
|---|
| 182 | |
|---|
| 183 | def auto_link_urls(text, **href_options): |
|---|
| 184 | extra_options = tag_options(**href_options) |
|---|
| 185 | def handle_match(matchobj): |
|---|
| 186 | all = matchobj.group() |
|---|
| 187 | a, b, c, d = matchobj.group(1,2,3,5) |
|---|
| 188 | if re.match(r'<a\s', a, re.I): |
|---|
| 189 | return all |
|---|
| 190 | text = b + c |
|---|
| 191 | if b == "www.": |
|---|
| 192 | b = "http://www." |
|---|
| 193 | return '%s<a href="%s%s"%s>%s</a>%s' % (a, b, c, extra_options, text, d) |
|---|
| 194 | return re.sub(AUTO_LINK_RE, handle_match, text) |
|---|
| 195 | |
|---|
| 196 | def auto_link_email_addresses(text): |
|---|
| 197 | def fix_email(match): |
|---|
| 198 | text = matchobj.group() |
|---|
| 199 | return '<a href="mailto:%s>%s</a>' % (text, text) |
|---|
| 200 | return re.sub(r'([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)', r'<a href="mailto:\1">\1</a>', text) |
|---|
| 201 | |
|---|
| 202 | def strip_links(text): |
|---|
| 203 | """ |
|---|
| 204 | Turns all links into words |
|---|
| 205 | |
|---|
| 206 | Example:: |
|---|
| 207 | |
|---|
| 208 | >>> strip_links("<a href="something">else</a>") |
|---|
| 209 | "else" |
|---|
| 210 | """ |
|---|
| 211 | return re.sub(r'<a\b.*?>(.*?)<\/a>', r'\1', text, re.M) |
|---|
| 212 | |
|---|
| 213 | def textilize(text, sanitize=False): |
|---|
| 214 | """Format the text with Textile formatting |
|---|
| 215 | |
|---|
| 216 | This function uses the `PyTextile library <http://dealmeida.net/>`_ which is included with WebHelpers. |
|---|
| 217 | |
|---|
| 218 | Additionally, the output can be sanitized which will fix tags like <img />, |
|---|
| 219 | <br /> and <hr /> for proper XHTML output. |
|---|
| 220 | |
|---|
| 221 | """ |
|---|
| 222 | texer = textile.Textiler(text) |
|---|
| 223 | return texer.process(sanitize=sanitize) |
|---|
| 224 | |
|---|
| 225 | def markdown(text): |
|---|
| 226 | """Format the text with MarkDown formatting |
|---|
| 227 | |
|---|
| 228 | This function uses the `Python MarkDown library <http://www.freewisdom.org/projects/python-markdown/>`_ |
|---|
| 229 | which is included with WebHelpers. |
|---|
| 230 | |
|---|
| 231 | """ |
|---|
| 232 | return markdown.markdown(text) |
|---|
| 233 | |
|---|
| 234 | __all__ = ['cycle', 'reset_cycle', 'truncate', 'highlight', 'excerpt', 'word_wrap', 'simple_format', |
|---|
| 235 | 'auto_link', 'strip_links', 'textilize', 'markdown'] |
|---|