1 # Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
2 # Gianluca Montecchi <gian@grys.it>
3 # Mathieu Clabaut <mathieu.clabaut@gmail.com>
4 # W. Trevor King <wking@drexel.edu>
6 # This file is part of Bugs Everywhere.
8 # Bugs Everywhere is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by the Free
10 # Software Foundation, either version 2 of the License, or (at your option) any
13 # Bugs Everywhere is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
18 # You should have received a copy of the GNU General Public License along with
19 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
29 import xml.sax.saxutils
31 from jinja2 import Environment, FileSystemLoader, DictLoader, ChoiceLoader
35 import libbe.command.depend
36 import libbe.command.util
38 import libbe.util.encoding
40 import libbe.util.wsgi
44 class ServerApp (libbe.util.wsgi.WSGI_AppObject,
45 libbe.util.wsgi.WSGI_DataObject):
46 """WSGI server for a BE Storage instance over HTML.
48 Serve browsable HTML for public consumption. Currently everything
51 server_version = 'BE-html-server/' + libbe.version.version()
53 def __init__(self, bugdirs={}, template_dir=None, title='Site Title',
54 header='Header', index_file='', min_id_length=-1,
55 generation_time=None, **kwargs):
56 super(ServerApp, self).__init__(
58 (r'^{}$'.format(index_file), self.index),
59 (r'^style.css$', self.style),
60 (r'^([^/]+)/([^/]+)/{}'.format(index_file), self.bug),
63 self.bugdirs = bugdirs
66 self._index_file = index_file
67 self.min_id_length = min_id_length
68 self.generation_time = generation_time
70 self.http_user_error = 418
71 self._load_templates(template_dir=template_dir)
73 'active': lambda bug: bug.active and bug.severity != 'target',
74 'inactive': lambda bug: not bug.active and bug.severity !='target',
75 'target': lambda bug: bug.severity == 'target'
79 def style(self, environ, start_response):
80 template = self.template.get_template('style.css')
81 content = template.render()
82 return self.ok_response(
83 environ, start_response, content, content_type='text/css')
85 def index(self, environ, start_response):
86 data = self.query_data(environ)
88 bug_type = self.data_get_string(
89 data, 'type', default='active', source=source)
90 assert bug_type in ['active', 'inactive', 'target'], bug_type
92 filter_ = self._filters.get(bug_type, self._filters['active'])
93 bugs = list(itertools.chain(*list(
94 [bug for bug in bugdir if filter_(bug)]
95 for bugdir in self.bugdirs.values())))
99 self.log_level, 'generate {} index file for {} bugs'.format(
100 bug_type, len(bugs)))
104 'stylesheet': 'style.css',
105 'header': self.header,
106 'active_class': 'tab nsel',
107 'inactive_class': 'tab nsel',
108 'target_class': 'tab nsel',
110 'bug_entry': self.template.get_template('index_bug_entry.html'),
111 'bug_dir': self.bug_dir,
112 'index_file': self._index_file,
113 'generation_time': self._generation_time(),
115 template_info['{}_class'.format(bug_type)] = 'tab sel'
116 if bug_type == 'target':
117 template = self.template.get_template('target_index.html')
118 template_info['targets'] = [
119 (target, sorted(libbe.command.depend.get_blocked_by(
120 self.bugdirs, target)))
123 template = self.template.get_template('standard_index.html')
124 content = template.render(template_info)+'\n'
125 return self.ok_response(
126 environ, start_response, content, content_type='text/html')
128 def bug(self, environ, start_response):
130 bugdir_id,bug_id = environ['be-server.url_args']
132 raise libbe.util.wsgi.HandlerError(404, 'Not Found')
133 user_id = '{}/{}'.format(bugdir_id, bug_id)
134 bugdir,bug,comment = (
135 libbe.command.util.bugdir_bug_comment_from_user_id(
136 self.bugdirs, user_id))
139 self.log_level, 'generate bug file for {}/{}'.format(
140 bugdir.uuid, bug.uuid))
141 if bug.severity == 'target':
142 index_type = 'target'
144 index_type = 'active'
146 index_type = 'inactive'
147 up_link = '../../{}?type={}'.format(self._index_file, index_type)
148 bug.load_comments(load_full=True)
149 bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
153 'stylesheet': '../../style.css',
154 'header': self.header,
155 'backlinks': self.template.get_template('bug_backlinks.html'),
157 'index_type': index_type.capitalize(),
158 'index_file': self._index_file,
160 'comment_entry': self.template.get_template(
161 'bug_comment_entry.html'),
162 'comments': [(depth,comment) for depth,comment
163 in bug.comment_root.thread(flatten=False)],
164 'comment_dir': self._truncated_comment_id,
165 'format_body': self._format_comment_body,
166 'div_close': _DivCloser(),
167 'generation_time': self._generation_time(),
169 template = self.template.get_template('bug.html')
170 content = template.render(template_info)
171 return self.ok_response(
172 environ, start_response, content, content_type='text/html')
176 if time.time() > self._refresh:
178 self.logger.log(self.log_level, 'refresh bugdirs')
179 for bugdir in self.bugdirs.values():
180 bugdir.load_all_bugs()
181 self._refresh = time.time() + 60
183 def _truncated_bugdir_id(self, bugdir):
184 return libbe.util.id._truncate(
185 bugdir.uuid, self.bugdirs.keys(),
186 min_length=self.min_id_length)
188 def _truncated_bug_id(self, bug):
189 return libbe.util.id._truncate(
190 bug.uuid, bug.sibling_uuids(),
191 min_length=self.min_id_length)
193 def _truncated_comment_id(self, comment):
194 return libbe.util.id._truncate(
195 comment.uuid, comment.sibling_uuids(),
196 min_length=self.min_id_length)
198 def bug_dir(self, bug):
199 return '{}/{}'.format(
200 self._truncated_bugdir_id(bug.bugdir),
201 self._truncated_bug_id(bug))
203 def _long_to_linked_user(self, text):
205 >>> import libbe.bugdir
206 >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
207 >>> a = ServerApp(bugdirs={bugdir.uuid: bugdir})
208 >>> a._long_to_linked_user('A link #abc123/a#, and a non-link #x#y#.')
209 'A link <a href="./a/">abc/a</a>, and a non-link #x#y#.'
212 replacer = libbe.util.id.IDreplacer(
213 self.bugdirs, self._long_to_linked_user_replacer, wrap=False)
215 libbe.util.id.REGEXP, replacer, text)
217 def _long_to_linked_user_replacer(self, bugdirs, long_id):
219 >>> import libbe.bugdir
220 >>> import libbe.util.id
221 >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
222 >>> bugdirs = {bugdir.uuid: bugdir}
223 >>> a = bugdir.bug_from_uuid('a')
224 >>> uuid_gen = libbe.util.id.uuid_gen
225 >>> libbe.util.id.uuid_gen = lambda : '0123'
226 >>> c = a.new_comment('comment for link testing')
227 >>> libbe.util.id.uuid_gen = uuid_gen
230 >>> a = ServerApp(bugdirs=bugdirs)
231 >>> a._long_to_linked_user_replacer(bugdirs, 'abc123')
233 >>> a._long_to_linked_user_replacer(bugdirs, 'abc123/a')
234 '<a href="./a/">abc/a</a>'
235 >>> a._long_to_linked_user_replacer(bugdirs, 'abc123/a/0123')
236 '<a href="./a/#0123">abc/a/012</a>'
237 >>> a._long_to_linked_user_replacer(bugdirs, 'x')
239 >>> a._long_to_linked_user_replacer(bugdirs, '')
244 p = libbe.util.id.parse_user(bugdirs, long_id)
245 except (libbe.util.id.MultipleIDMatches,
246 libbe.util.id.NoIDMatches,
247 libbe.util.id.InvalidIDStructure), e:
248 return '#%s#' % long_id # re-wrap failures
249 if p['type'] == 'bugdir':
250 return '#%s#' % long_id
251 elif p['type'] == 'bug':
252 bugdir,bug,comment = (
253 libbe.command.util.bugdir_bug_comment_from_user_id(
255 return '<a href="./%s/">%s</a>' \
256 % (self._truncated_bug_id(bug), bug.id.user())
257 elif p['type'] == 'comment':
258 bugdir,bug,comment = (
259 libbe.command.util.bugdir_bug_comment_from_user_id(
261 return '<a href="./%s/#%s">%s</a>' \
262 % (self._truncated_bug_id(bug),
263 self._truncated_comment_id(comment),
265 raise Exception('Invalid id type %s for "%s"'
266 % (p['type'], long_id))
268 def _format_comment_body(self, bug, comment):
269 link_long_ids = False
272 if comment.content_type == 'text/html':
274 elif comment.content_type.startswith('text/'):
275 value = '<pre>\n'+self._escape(value)+'\n</pre>'
277 elif comment.content_type.startswith('image/'):
279 value = '<img src="./%s/%s" />' % (
280 self._truncated_bug_id(bug),
281 self._truncated_comment_id(comment))
284 value = '<a href="./%s/%s">Link to %s file</a>.' % (
285 self._truncated_bug_id(bug),
286 self._truncated_comment_id(comment),
287 comment.content_type)
288 if link_long_ids == True:
289 value = self._long_to_linked_user(value)
290 if save_body == True:
291 per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
292 if not os.path.exists(per_bug_dir):
293 os.mkdir(per_bug_dir)
294 comment_path = os.path.join(per_bug_dir, comment.uuid)
296 '<Files %s>\n ForceType %s\n</Files>' \
297 % (comment.uuid, comment.content_type),
298 [per_bug_dir, '.htaccess'], mode='a')
299 self._write_file(comment.body,
300 [per_bug_dir, comment.uuid], mode='wb')
303 def _generation_time(self):
304 if self.generation_time:
305 return self.generation_time
308 def _escape(self, string):
311 return xml.sax.saxutils.escape(string)
313 def _load_templates(self, template_dir=None):
314 if template_dir is not None:
315 template_dir = os.path.abspath(os.path.expanduser(template_dir))
317 self.template_dict = {
321 font-family: "lucida grande", "sans serif";
333 background-color: #fcfcfc;
334 -moz-border-radius: 10px;
347 -moz-border-radius: 10px;
356 padding-bottom: 10px;
359 -moz-border-radius: 10px;
365 border-color: #305275;
366 background-color: #305275;
369 -moz-border-radius-topleft: 8px;
370 -moz-border-radius-topright: 8px;
378 border-spacing: 0px 0px;
389 border-style: dotted;
397 border-color: #c3d9ff;
398 border-collapse: collapse;
406 border-color: #c3d9ff;
407 border-collapse: collapse;
413 img { border-style: none; }
416 list-style-type: none;
429 text-decoration: none;
432 a { color: #553d41; }
433 a:hover { color: #003d41; }
434 a:visited { color: #305275; }
435 .footer a { color: #508d91; }
437 /* bug index pages */
445 background-color: #c3d9ff ;
446 border: 1px solid #c3d9ff;
448 -moz-border-radius-topleft: 15px;
449 -moz-border-radius-topright: 15px;
453 border: 1px solid #c3d9ff;
455 -moz-border-radius-topleft: 5px;
456 -moz-border-radius-topright: 5px;
462 border-color: #c3d9ff;
465 border: 1px solid #c3d9ff;
474 table.target_list.td {
478 tr.wishlist { background-color: #DCFAFF;}
479 tr.wishlist:hover { background-color: #C2DCE1; }
481 tr.minor { background-color: #FFFFA6; }
482 tr.minor:hover { background-color: #E6E696; }
484 tr.serious { background-color: #FF9077;}
485 tr.serious:hover { background-color: #E6826B; }
487 tr.critical { background-color: #FF752A; }
488 tr.critical:hover { background-color: #D63905;}
490 tr.fatal { background-color: #FF3300;}
491 tr.fatal:hover { background-color: #D60000;}
493 td.uuid { width: 5%; border-style: dotted;}
494 td.status { width: 5%; border-style: dotted;}
495 td.severity { width: 5%; border-style: dotted;}
496 td.summary { border-style: dotted;}
497 td.date { width: 25%; border-style: dotted;}
499 /* bug detail pages */
501 td.bug_detail_label { text-align: right; border: none;}
502 td.bug_detail { border: none;}
503 td.bug_comment_label { text-align: right; vertical-align: top; }
515 /* padding-top: 0px; */
516 padding-bottom: 20px;
521 """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
522 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
523 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
525 <title>{{ title }}</title>
526 <meta http-equiv="Content-Type" content="text/html; charset={{ charset }}" />
527 <link rel="stylesheet" href="{{ stylesheet }}" type="text/css" />
530 <div class="header">{{ header }}</div>
532 {% block content %}{% endblock %}
535 <p>Generated by <a href="http://www.bugseverywhere.org/">
536 Bugs Everywhere</a> on {{ generation_time }}</p>
538 <a href="http://validator.w3.org/check?uri=referer">
539 Validate XHTML</a> |
540 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">
549 """{% extends "base.html" %}
553 {% block bugdir_table %}{% endblock %}
561 """{% extends "base.html" %}
567 <td class="{{ active_class }}"><a href="{% if index_file %}{{ index_file }}{% else %}.{% endif %}">Active Bugs</a></td>
568 <td class="{{ inactive_class }}"><a href="{{ index_file }}?type=inactive">Inactive Bugs</a></td>
569 <td class="{{ target_class }}"><a href="{{ index_file }}?type=target">Divided by target</a></td>
574 {% block bug_table %}{% endblock %}
581 'standard_index.html':
582 """{% extends "index.html" %}
584 {% block bug_table %}
585 <table class="bug_list">
596 {% for bug in bugs %}
597 {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug), 'index_file':index_file}) }}
605 """{% extends "index.html" %}
607 {% block bug_table %}
608 {% for target,bugs in targets %}
609 <table class="target_list">
612 <th class="target_name" colspan="5">
613 Target: {{ target.summary|e }} ({{ target.status|e }})
625 {% for bug in bugs %}
626 {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug), 'index_file':index_file}) }}
634 'index_bug_entry.html':
635 """<tr class="{{ bug.severity }}">
636 <td class="uuid"><a href="{{ dir }}/{{ index_file }}">{{ bug.id.user()|e }}</a></td>
637 <td class="status"><a href="{{ dir }}/{{ index_file }}">{{ bug.status|e }}</a></td>
638 <td class="severity"><a href="{{ dir }}/{{ index_file }}">{{ bug.severity|e }}</a></td>
639 <td class="summary"><a href="{{ dir }}/{{ index_file }}">{{ bug.summary|e }}</a></td>
640 <td class="date"><a href="{{ dir }}/{{ index_file }}">{{ (bug.time_string or '')|e }}</a></td>
645 """{% extends "base.html" %}
648 {{ backlinks.render({'up_link': up_link, 'index_type':index_type, 'index_file':index_file}) }}
649 <h1>Bug: {{ bug.id.user()|e }}</h1>
653 <tr><td class="bug_detail_label">ID :</td>
654 <td class="bug_detail">{{ bug.uuid|e }}</td></tr>
655 <tr><td class="bug_detail_label">Short name :</td>
656 <td class="bug_detail">{{ bug.id.user()|e }}</td></tr>
657 <tr><td class="bug_detail_label">Status :</td>
658 <td class="bug_detail">{{ bug.status|e }}</td></tr>
659 <tr><td class="bug_detail_label">Severity :</td>
660 <td class="bug_detail">{{ bug.severity|e }}</td></tr>
661 <tr><td class="bug_detail_label">Assigned :</td>
662 <td class="bug_detail">{{ (bug.assigned or '')|e }}</td></tr>
663 <tr><td class="bug_detail_label">Reporter :</td>
664 <td class="bug_detail">{{ (bug.reporter or '')|e }}</td></tr>
665 <tr><td class="bug_detail_label">Creator :</td>
666 <td class="bug_detail">{{ (bug.creator or '')|e }}</td></tr>
667 <tr><td class="bug_detail_label">Created :</td>
668 <td class="bug_detail">{{ (bug.time_string or '')|e }}</td></tr>
669 <tr><td class="bug_detail_label">Summary :</td>
670 <td class="bug_detail">{{ bug.summary|e }}</td></tr>
677 {% for depth,comment in comments %}
679 <div class="comment root" id="C{{ comment_dir(comment) }}">
681 <div class="comment" id="C{{ comment_dir(comment) }}">
683 {{ comment_entry.render({
684 'depth':depth, 'bug': bug, 'comment':comment, 'comment_dir':comment_dir,
685 'format_body': format_body, 'div_close': div_close}) }}
686 {{ div_close(depth) }}
688 {% if comments[-1][0] > 0 %}
694 {{ backlinks.render({'up_link': up_link, 'index_type': index_type}) }}
698 'bug_backlinks.html':
699 """<p class="backlink"><a href="{{ up_link }}">Back to {{ index_type }} Index</a></p>
700 <p class="backlink"><a href="../../{{ index_file }}?type=target">Back to Target Index</a></p>
703 'bug_comment_entry.html':
707 <td class="bug_comment_label">Comment:</td>
708 <td class="bug_comment">
709 --------- Comment ---------<br/>
710 ID: {{ comment.uuid }}<br/>
711 Short name: {{ comment.id.user() }}<br/>
712 From: {{ (comment.author or '')|e }}<br/>
713 Date: {{ (comment.date or '')|e }}<br/>
715 {{ format_body(bug, comment) }}
723 loader = DictLoader(self.template_dict)
726 file_system_loader = FileSystemLoader(template_dir)
727 loader = ChoiceLoader([file_system_loader, loader])
728 self.template = Environment(loader=loader)
731 class HTML (libbe.util.wsgi.ServerCommand):
732 """Serve or dump browsable HTML for the current repository
735 >>> import libbe.bugdir
736 >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
737 >>> io = libbe.command.StringInputOutput()
738 >>> io.stdout = sys.stdout
739 >>> ui = libbe.command.UserInterface(io=io)
740 >>> ui.storage_callbacks.set_storage(bugdir.storage)
741 >>> cmd = HTML(ui=ui)
743 >>> export_path = os.path.join(bugdir.storage.repo, 'html_export')
744 >>> ret = ui.run(cmd, {'output': export_path, 'export-html': True})
745 >>> os.path.exists(export_path)
747 >>> os.path.exists(os.path.join(export_path, 'index.html'))
749 >>> os.path.exists(os.path.join(export_path, 'index_inactive.html'))
751 >>> os.path.exists(os.path.join(export_path, bugdir.uuid))
753 >>> for bug in sorted(bugdir):
754 ... if os.path.exists(os.path.join(
755 ... export_path, bugdir.uuid, bug.uuid, 'index.html')):
756 ... print('got {}'.format(bug.uuid))
758 ... print('missing {}'.format(bug.uuid))
767 def __init__(self, *args, **kwargs):
768 super(HTML, self).__init__(*args, **kwargs)
769 # ServerApp cannot write, so drop some security options
771 option for option in self.options
772 if option.name not in [
778 self.options.extend([
779 libbe.command.Option(name='template-dir', short_name='t',
780 help=('Use different templates. Defaults to internal '
782 arg=libbe.command.Argument(
783 name='template-dir', metavar='DIR',
784 completion_callback=libbe.command.util.complete_path)),
785 libbe.command.Option(name='title',
786 help='Set the bug repository title (%default)',
787 arg=libbe.command.Argument(
788 name='title', metavar='STRING',
789 default='Bugs Everywhere Issue Tracker')),
790 libbe.command.Option(name='index-header',
791 help='Set the index page headers (%default)',
792 arg=libbe.command.Argument(
793 name='index-header', metavar='STRING',
794 default='Bugs Everywhere Bug List')),
795 libbe.command.Option(name='min-id-length', short_name='l',
796 help=('Attempt to truncate bug and comment IDs to this '
797 'length. Set to -1 for non-truncated IDs '
799 arg=libbe.command.Argument(
800 name='min-id-length', metavar='INT',
801 default=-1, type='int')),
802 libbe.command.Option(name='export-html', short_name='e',
803 help='Export all HTML pages and exit.'),
804 libbe.command.Option(name='output', short_name='o',
805 help='Set the output path for HTML export (%default)',
806 arg=libbe.command.Argument(
807 name='output', metavar='DIR', default='./html_export',
808 completion_callback=libbe.command.util.complete_path)),
809 libbe.command.Option(name='export-template', short_name='E',
810 help='Export the default template and exit.'),
811 libbe.command.Option(name='export-template-dir', short_name='d',
812 help='Set the directory for the template export (%default)',
813 arg=libbe.command.Argument(
814 name='export-template-dir', metavar='DIR',
815 default='./default-templates/',
816 completion_callback=libbe.command.util.complete_path)),
819 def _run(self, **params):
820 if True in [params['export-template'], params['export-html']]:
822 logger=None, storage=None, index_file='index.html',
823 generation_time=time.ctime(), **params)
824 if params['export-template']:
825 self._write_default_template(
826 template_dict=app.template_dict,
827 out_dir=params['export-template-dir'])
828 elif params['export-html']:
829 self._write_static_pages(app=app, out_dir=params['output'])
831 # provide defaults for the dropped options
832 params['read-only'] = True
833 params['notify'] = None
834 params['auth'] = None
835 return super(HTML, self)._run(**params)
837 def _get_app(self, logger, storage, index_file='', generation_time=None,
840 logger=logger, bugdirs=self._get_bugdirs(),
841 template_dir=kwargs['template-dir'],
842 title=kwargs['title'],
843 header=kwargs['index-header'],
844 index_file=index_file,
845 min_id_length=kwargs['min-id-length'],
846 generation_time=generation_time)
848 def _long_help(self):
854 Then point your browser at ``http://localhost:8000/``.
856 If either ``--export-html`` or ``export-template`` is set, the command
857 will exit after the dump without serving anything over the wire.
860 def _write_default_template(self, template_dict, out_dir):
861 out_dir = self._make_dir(out_dir)
862 for filename,text in template_dict.iteritems():
863 self._write_file(text, [out_dir, filename])
865 def _write_static_pages(self, app, out_dir):
867 ('index.html?type=active', 'index.html'),
868 ('index.html?type=inactive', 'index_inactive.html'),
869 ('index.html?type=target', 'index_by_target.html'),
871 out_dir = self._make_dir(out_dir)
872 caller = libbe.util.wsgi.WSGICaller()
874 content=self._get_content(caller, app, 'style.css'),
875 path_array=[out_dir, 'style.css'])
876 for url,data_dict,path in [
877 ('index.html', {'type': 'active'}, 'index.html'),
878 ('index.html', {'type': 'inactive'}, 'index_inactive.html'),
879 ('index.html', {'type': 'target'}, 'index_by_target.html'),
881 content = self._get_content(caller, app, url, data_dict)
882 for url_,path_ in url_mappings:
883 content = content.replace(url_, path_)
884 self._write_file(content=content, path_array=[out_dir, path])
885 for bugdir in app.bugdirs.values():
887 bug_dir_url = app.bug_dir(bug=bug)
888 segments = bug_dir_url.split('/')
889 path_array = [out_dir]
890 path_array.extend(segments)
891 bug_dir_path = os.path.join(*path_array)
892 path_array.append(app._index_file)
893 url = '{}/{}'.format(bug_dir_url, app._index_file)
894 content = self._get_content(caller, app, url)
895 for url_,path_ in url_mappings:
896 content = content.replace(url_, path_)
897 if not os.path.isdir(bug_dir_path):
898 self._make_dir(bug_dir_path)
899 self._write_file(content=content, path_array=path_array)
901 def _get_content(self, caller, app, path, data_dict=None):
903 return caller.getURL(app=app, path=path, data_dict=data_dict)
904 except libbe.util.wsgi.HandlerError:
906 'error retrieving {} with {}\n'.format(path, data_dict))
909 def _make_dir(self, dir_path):
910 dir_path = os.path.abspath(os.path.expanduser(dir_path))
911 if not os.path.exists(dir_path):
913 os.makedirs(dir_path)
915 raise libbe.command.UserError(
916 'Cannot create output directory "{}".'.format(dir_path))
919 def _write_file(self, content, path_array, mode='w'):
920 if not hasattr(self, 'encoding'):
921 self.encoding = libbe.util.encoding.get_text_file_encoding()
922 return libbe.util.encoding.set_file_contents(
923 os.path.join(*path_array), content, mode, self.encoding)
926 Html = HTML # alias for libbe.command.base.get_command_class()
929 class _DivCloser (object):
930 def __init__(self, depth=0):
933 def __call__(self, depth):
935 while self.depth >= depth:
939 return '\n'.join(ret)