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'] |
---|