##// END OF EJS Templates
help: reorder section filtering flow to not format help text twice
Yuya Nishihara -
r39345:0a766cb1 default
parent child Browse files
Show More
@@ -1,693 +1,690 b''
1 # help.py - help data for mercurial
1 # help.py - help data for mercurial
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import itertools
10 import itertools
11 import os
11 import os
12 import textwrap
12 import textwrap
13
13
14 from .i18n import (
14 from .i18n import (
15 _,
15 _,
16 gettext,
16 gettext,
17 )
17 )
18 from . import (
18 from . import (
19 cmdutil,
19 cmdutil,
20 encoding,
20 encoding,
21 error,
21 error,
22 extensions,
22 extensions,
23 fancyopts,
23 fancyopts,
24 filemerge,
24 filemerge,
25 fileset,
25 fileset,
26 minirst,
26 minirst,
27 pycompat,
27 pycompat,
28 revset,
28 revset,
29 templatefilters,
29 templatefilters,
30 templatefuncs,
30 templatefuncs,
31 templatekw,
31 templatekw,
32 util,
32 util,
33 )
33 )
34 from .hgweb import (
34 from .hgweb import (
35 webcommands,
35 webcommands,
36 )
36 )
37
37
38 _exclkeywords = {
38 _exclkeywords = {
39 "(ADVANCED)",
39 "(ADVANCED)",
40 "(DEPRECATED)",
40 "(DEPRECATED)",
41 "(EXPERIMENTAL)",
41 "(EXPERIMENTAL)",
42 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
42 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
43 _("(ADVANCED)"),
43 _("(ADVANCED)"),
44 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
44 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
45 _("(DEPRECATED)"),
45 _("(DEPRECATED)"),
46 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
46 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
47 _("(EXPERIMENTAL)"),
47 _("(EXPERIMENTAL)"),
48 }
48 }
49
49
50 def listexts(header, exts, indent=1, showdeprecated=False):
50 def listexts(header, exts, indent=1, showdeprecated=False):
51 '''return a text listing of the given extensions'''
51 '''return a text listing of the given extensions'''
52 rst = []
52 rst = []
53 if exts:
53 if exts:
54 for name, desc in sorted(exts.iteritems()):
54 for name, desc in sorted(exts.iteritems()):
55 if not showdeprecated and any(w in desc for w in _exclkeywords):
55 if not showdeprecated and any(w in desc for w in _exclkeywords):
56 continue
56 continue
57 rst.append('%s:%s: %s\n' % (' ' * indent, name, desc))
57 rst.append('%s:%s: %s\n' % (' ' * indent, name, desc))
58 if rst:
58 if rst:
59 rst.insert(0, '\n%s\n\n' % header)
59 rst.insert(0, '\n%s\n\n' % header)
60 return rst
60 return rst
61
61
62 def extshelp(ui):
62 def extshelp(ui):
63 rst = loaddoc('extensions')(ui).splitlines(True)
63 rst = loaddoc('extensions')(ui).splitlines(True)
64 rst.extend(listexts(
64 rst.extend(listexts(
65 _('enabled extensions:'), extensions.enabled(), showdeprecated=True))
65 _('enabled extensions:'), extensions.enabled(), showdeprecated=True))
66 rst.extend(listexts(_('disabled extensions:'), extensions.disabled(),
66 rst.extend(listexts(_('disabled extensions:'), extensions.disabled(),
67 showdeprecated=ui.verbose))
67 showdeprecated=ui.verbose))
68 doc = ''.join(rst)
68 doc = ''.join(rst)
69 return doc
69 return doc
70
70
71 def optrst(header, options, verbose):
71 def optrst(header, options, verbose):
72 data = []
72 data = []
73 multioccur = False
73 multioccur = False
74 for option in options:
74 for option in options:
75 if len(option) == 5:
75 if len(option) == 5:
76 shortopt, longopt, default, desc, optlabel = option
76 shortopt, longopt, default, desc, optlabel = option
77 else:
77 else:
78 shortopt, longopt, default, desc = option
78 shortopt, longopt, default, desc = option
79 optlabel = _("VALUE") # default label
79 optlabel = _("VALUE") # default label
80
80
81 if not verbose and any(w in desc for w in _exclkeywords):
81 if not verbose and any(w in desc for w in _exclkeywords):
82 continue
82 continue
83
83
84 so = ''
84 so = ''
85 if shortopt:
85 if shortopt:
86 so = '-' + shortopt
86 so = '-' + shortopt
87 lo = '--' + longopt
87 lo = '--' + longopt
88
88
89 if isinstance(default, fancyopts.customopt):
89 if isinstance(default, fancyopts.customopt):
90 default = default.getdefaultvalue()
90 default = default.getdefaultvalue()
91 if default and not callable(default):
91 if default and not callable(default):
92 # default is of unknown type, and in Python 2 we abused
92 # default is of unknown type, and in Python 2 we abused
93 # the %s-shows-repr property to handle integers etc. To
93 # the %s-shows-repr property to handle integers etc. To
94 # match that behavior on Python 3, we do str(default) and
94 # match that behavior on Python 3, we do str(default) and
95 # then convert it to bytes.
95 # then convert it to bytes.
96 desc += _(" (default: %s)") % pycompat.bytestr(default)
96 desc += _(" (default: %s)") % pycompat.bytestr(default)
97
97
98 if isinstance(default, list):
98 if isinstance(default, list):
99 lo += " %s [+]" % optlabel
99 lo += " %s [+]" % optlabel
100 multioccur = True
100 multioccur = True
101 elif (default is not None) and not isinstance(default, bool):
101 elif (default is not None) and not isinstance(default, bool):
102 lo += " %s" % optlabel
102 lo += " %s" % optlabel
103
103
104 data.append((so, lo, desc))
104 data.append((so, lo, desc))
105
105
106 if multioccur:
106 if multioccur:
107 header += (_(" ([+] can be repeated)"))
107 header += (_(" ([+] can be repeated)"))
108
108
109 rst = ['\n%s:\n\n' % header]
109 rst = ['\n%s:\n\n' % header]
110 rst.extend(minirst.maketable(data, 1))
110 rst.extend(minirst.maketable(data, 1))
111
111
112 return ''.join(rst)
112 return ''.join(rst)
113
113
114 def indicateomitted(rst, omitted, notomitted=None):
114 def indicateomitted(rst, omitted, notomitted=None):
115 rst.append('\n\n.. container:: omitted\n\n %s\n\n' % omitted)
115 rst.append('\n\n.. container:: omitted\n\n %s\n\n' % omitted)
116 if notomitted:
116 if notomitted:
117 rst.append('\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
117 rst.append('\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
118
118
119 def filtercmd(ui, cmd, kw, doc):
119 def filtercmd(ui, cmd, kw, doc):
120 if not ui.debugflag and cmd.startswith("debug") and kw != "debug":
120 if not ui.debugflag and cmd.startswith("debug") and kw != "debug":
121 return True
121 return True
122 if not ui.verbose and doc and any(w in doc for w in _exclkeywords):
122 if not ui.verbose and doc and any(w in doc for w in _exclkeywords):
123 return True
123 return True
124 return False
124 return False
125
125
126 def topicmatch(ui, commands, kw):
126 def topicmatch(ui, commands, kw):
127 """Return help topics matching kw.
127 """Return help topics matching kw.
128
128
129 Returns {'section': [(name, summary), ...], ...} where section is
129 Returns {'section': [(name, summary), ...], ...} where section is
130 one of topics, commands, extensions, or extensioncommands.
130 one of topics, commands, extensions, or extensioncommands.
131 """
131 """
132 kw = encoding.lower(kw)
132 kw = encoding.lower(kw)
133 def lowercontains(container):
133 def lowercontains(container):
134 return kw in encoding.lower(container) # translated in helptable
134 return kw in encoding.lower(container) # translated in helptable
135 results = {'topics': [],
135 results = {'topics': [],
136 'commands': [],
136 'commands': [],
137 'extensions': [],
137 'extensions': [],
138 'extensioncommands': [],
138 'extensioncommands': [],
139 }
139 }
140 for names, header, doc in helptable:
140 for names, header, doc in helptable:
141 # Old extensions may use a str as doc.
141 # Old extensions may use a str as doc.
142 if (sum(map(lowercontains, names))
142 if (sum(map(lowercontains, names))
143 or lowercontains(header)
143 or lowercontains(header)
144 or (callable(doc) and lowercontains(doc(ui)))):
144 or (callable(doc) and lowercontains(doc(ui)))):
145 results['topics'].append((names[0], header))
145 results['topics'].append((names[0], header))
146 for cmd, entry in commands.table.iteritems():
146 for cmd, entry in commands.table.iteritems():
147 if len(entry) == 3:
147 if len(entry) == 3:
148 summary = entry[2]
148 summary = entry[2]
149 else:
149 else:
150 summary = ''
150 summary = ''
151 # translate docs *before* searching there
151 # translate docs *before* searching there
152 docs = _(pycompat.getdoc(entry[0])) or ''
152 docs = _(pycompat.getdoc(entry[0])) or ''
153 if kw in cmd or lowercontains(summary) or lowercontains(docs):
153 if kw in cmd or lowercontains(summary) or lowercontains(docs):
154 doclines = docs.splitlines()
154 doclines = docs.splitlines()
155 if doclines:
155 if doclines:
156 summary = doclines[0]
156 summary = doclines[0]
157 cmdname = cmdutil.parsealiases(cmd)[0]
157 cmdname = cmdutil.parsealiases(cmd)[0]
158 if filtercmd(ui, cmdname, kw, docs):
158 if filtercmd(ui, cmdname, kw, docs):
159 continue
159 continue
160 results['commands'].append((cmdname, summary))
160 results['commands'].append((cmdname, summary))
161 for name, docs in itertools.chain(
161 for name, docs in itertools.chain(
162 extensions.enabled(False).iteritems(),
162 extensions.enabled(False).iteritems(),
163 extensions.disabled().iteritems()):
163 extensions.disabled().iteritems()):
164 if not docs:
164 if not docs:
165 continue
165 continue
166 name = name.rpartition('.')[-1]
166 name = name.rpartition('.')[-1]
167 if lowercontains(name) or lowercontains(docs):
167 if lowercontains(name) or lowercontains(docs):
168 # extension docs are already translated
168 # extension docs are already translated
169 results['extensions'].append((name, docs.splitlines()[0]))
169 results['extensions'].append((name, docs.splitlines()[0]))
170 try:
170 try:
171 mod = extensions.load(ui, name, '')
171 mod = extensions.load(ui, name, '')
172 except ImportError:
172 except ImportError:
173 # debug message would be printed in extensions.load()
173 # debug message would be printed in extensions.load()
174 continue
174 continue
175 for cmd, entry in getattr(mod, 'cmdtable', {}).iteritems():
175 for cmd, entry in getattr(mod, 'cmdtable', {}).iteritems():
176 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
176 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
177 cmdname = cmdutil.parsealiases(cmd)[0]
177 cmdname = cmdutil.parsealiases(cmd)[0]
178 cmddoc = pycompat.getdoc(entry[0])
178 cmddoc = pycompat.getdoc(entry[0])
179 if cmddoc:
179 if cmddoc:
180 cmddoc = gettext(cmddoc).splitlines()[0]
180 cmddoc = gettext(cmddoc).splitlines()[0]
181 else:
181 else:
182 cmddoc = _('(no help text available)')
182 cmddoc = _('(no help text available)')
183 if filtercmd(ui, cmdname, kw, cmddoc):
183 if filtercmd(ui, cmdname, kw, cmddoc):
184 continue
184 continue
185 results['extensioncommands'].append((cmdname, cmddoc))
185 results['extensioncommands'].append((cmdname, cmddoc))
186 return results
186 return results
187
187
188 def loaddoc(topic, subdir=None):
188 def loaddoc(topic, subdir=None):
189 """Return a delayed loader for help/topic.txt."""
189 """Return a delayed loader for help/topic.txt."""
190
190
191 def loader(ui):
191 def loader(ui):
192 docdir = os.path.join(util.datapath, 'help')
192 docdir = os.path.join(util.datapath, 'help')
193 if subdir:
193 if subdir:
194 docdir = os.path.join(docdir, subdir)
194 docdir = os.path.join(docdir, subdir)
195 path = os.path.join(docdir, topic + ".txt")
195 path = os.path.join(docdir, topic + ".txt")
196 doc = gettext(util.readfile(path))
196 doc = gettext(util.readfile(path))
197 for rewriter in helphooks.get(topic, []):
197 for rewriter in helphooks.get(topic, []):
198 doc = rewriter(ui, topic, doc)
198 doc = rewriter(ui, topic, doc)
199 return doc
199 return doc
200
200
201 return loader
201 return loader
202
202
203 internalstable = sorted([
203 internalstable = sorted([
204 (['bundle2'], _('Bundle2'),
204 (['bundle2'], _('Bundle2'),
205 loaddoc('bundle2', subdir='internals')),
205 loaddoc('bundle2', subdir='internals')),
206 (['bundles'], _('Bundles'),
206 (['bundles'], _('Bundles'),
207 loaddoc('bundles', subdir='internals')),
207 loaddoc('bundles', subdir='internals')),
208 (['censor'], _('Censor'),
208 (['censor'], _('Censor'),
209 loaddoc('censor', subdir='internals')),
209 loaddoc('censor', subdir='internals')),
210 (['changegroups'], _('Changegroups'),
210 (['changegroups'], _('Changegroups'),
211 loaddoc('changegroups', subdir='internals')),
211 loaddoc('changegroups', subdir='internals')),
212 (['config'], _('Config Registrar'),
212 (['config'], _('Config Registrar'),
213 loaddoc('config', subdir='internals')),
213 loaddoc('config', subdir='internals')),
214 (['requirements'], _('Repository Requirements'),
214 (['requirements'], _('Repository Requirements'),
215 loaddoc('requirements', subdir='internals')),
215 loaddoc('requirements', subdir='internals')),
216 (['revlogs'], _('Revision Logs'),
216 (['revlogs'], _('Revision Logs'),
217 loaddoc('revlogs', subdir='internals')),
217 loaddoc('revlogs', subdir='internals')),
218 (['wireprotocol'], _('Wire Protocol'),
218 (['wireprotocol'], _('Wire Protocol'),
219 loaddoc('wireprotocol', subdir='internals')),
219 loaddoc('wireprotocol', subdir='internals')),
220 ])
220 ])
221
221
222 def internalshelp(ui):
222 def internalshelp(ui):
223 """Generate the index for the "internals" topic."""
223 """Generate the index for the "internals" topic."""
224 lines = ['To access a subtopic, use "hg help internals.{subtopic-name}"\n',
224 lines = ['To access a subtopic, use "hg help internals.{subtopic-name}"\n',
225 '\n']
225 '\n']
226 for names, header, doc in internalstable:
226 for names, header, doc in internalstable:
227 lines.append(' :%s: %s\n' % (names[0], header))
227 lines.append(' :%s: %s\n' % (names[0], header))
228
228
229 return ''.join(lines)
229 return ''.join(lines)
230
230
231 helptable = sorted([
231 helptable = sorted([
232 (['bundlespec'], _("Bundle File Formats"), loaddoc('bundlespec')),
232 (['bundlespec'], _("Bundle File Formats"), loaddoc('bundlespec')),
233 (['color'], _("Colorizing Outputs"), loaddoc('color')),
233 (['color'], _("Colorizing Outputs"), loaddoc('color')),
234 (["config", "hgrc"], _("Configuration Files"), loaddoc('config')),
234 (["config", "hgrc"], _("Configuration Files"), loaddoc('config')),
235 (['deprecated'], _("Deprecated Features"), loaddoc('deprecated')),
235 (['deprecated'], _("Deprecated Features"), loaddoc('deprecated')),
236 (["dates"], _("Date Formats"), loaddoc('dates')),
236 (["dates"], _("Date Formats"), loaddoc('dates')),
237 (["flags"], _("Command-line flags"), loaddoc('flags')),
237 (["flags"], _("Command-line flags"), loaddoc('flags')),
238 (["patterns"], _("File Name Patterns"), loaddoc('patterns')),
238 (["patterns"], _("File Name Patterns"), loaddoc('patterns')),
239 (['environment', 'env'], _('Environment Variables'),
239 (['environment', 'env'], _('Environment Variables'),
240 loaddoc('environment')),
240 loaddoc('environment')),
241 (['revisions', 'revs', 'revsets', 'revset', 'multirevs', 'mrevs'],
241 (['revisions', 'revs', 'revsets', 'revset', 'multirevs', 'mrevs'],
242 _('Specifying Revisions'), loaddoc('revisions')),
242 _('Specifying Revisions'), loaddoc('revisions')),
243 (['filesets', 'fileset'], _("Specifying File Sets"), loaddoc('filesets')),
243 (['filesets', 'fileset'], _("Specifying File Sets"), loaddoc('filesets')),
244 (['diffs'], _('Diff Formats'), loaddoc('diffs')),
244 (['diffs'], _('Diff Formats'), loaddoc('diffs')),
245 (['merge-tools', 'mergetools', 'mergetool'], _('Merge Tools'),
245 (['merge-tools', 'mergetools', 'mergetool'], _('Merge Tools'),
246 loaddoc('merge-tools')),
246 loaddoc('merge-tools')),
247 (['templating', 'templates', 'template', 'style'], _('Template Usage'),
247 (['templating', 'templates', 'template', 'style'], _('Template Usage'),
248 loaddoc('templates')),
248 loaddoc('templates')),
249 (['urls'], _('URL Paths'), loaddoc('urls')),
249 (['urls'], _('URL Paths'), loaddoc('urls')),
250 (["extensions"], _("Using Additional Features"), extshelp),
250 (["extensions"], _("Using Additional Features"), extshelp),
251 (["subrepos", "subrepo"], _("Subrepositories"), loaddoc('subrepos')),
251 (["subrepos", "subrepo"], _("Subrepositories"), loaddoc('subrepos')),
252 (["hgweb"], _("Configuring hgweb"), loaddoc('hgweb')),
252 (["hgweb"], _("Configuring hgweb"), loaddoc('hgweb')),
253 (["glossary"], _("Glossary"), loaddoc('glossary')),
253 (["glossary"], _("Glossary"), loaddoc('glossary')),
254 (["hgignore", "ignore"], _("Syntax for Mercurial Ignore Files"),
254 (["hgignore", "ignore"], _("Syntax for Mercurial Ignore Files"),
255 loaddoc('hgignore')),
255 loaddoc('hgignore')),
256 (["phases"], _("Working with Phases"), loaddoc('phases')),
256 (["phases"], _("Working with Phases"), loaddoc('phases')),
257 (['scripting'], _('Using Mercurial from scripts and automation'),
257 (['scripting'], _('Using Mercurial from scripts and automation'),
258 loaddoc('scripting')),
258 loaddoc('scripting')),
259 (['internals'], _("Technical implementation topics"),
259 (['internals'], _("Technical implementation topics"),
260 internalshelp),
260 internalshelp),
261 (['pager'], _("Pager Support"), loaddoc('pager')),
261 (['pager'], _("Pager Support"), loaddoc('pager')),
262 ])
262 ])
263
263
264 # Maps topics with sub-topics to a list of their sub-topics.
264 # Maps topics with sub-topics to a list of their sub-topics.
265 subtopics = {
265 subtopics = {
266 'internals': internalstable,
266 'internals': internalstable,
267 }
267 }
268
268
269 # Map topics to lists of callable taking the current topic help and
269 # Map topics to lists of callable taking the current topic help and
270 # returning the updated version
270 # returning the updated version
271 helphooks = {}
271 helphooks = {}
272
272
273 def addtopichook(topic, rewriter):
273 def addtopichook(topic, rewriter):
274 helphooks.setdefault(topic, []).append(rewriter)
274 helphooks.setdefault(topic, []).append(rewriter)
275
275
276 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
276 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
277 """Extract docstring from the items key to function mapping, build a
277 """Extract docstring from the items key to function mapping, build a
278 single documentation block and use it to overwrite the marker in doc.
278 single documentation block and use it to overwrite the marker in doc.
279 """
279 """
280 entries = []
280 entries = []
281 for name in sorted(items):
281 for name in sorted(items):
282 text = (pycompat.getdoc(items[name]) or '').rstrip()
282 text = (pycompat.getdoc(items[name]) or '').rstrip()
283 if (not text
283 if (not text
284 or not ui.verbose and any(w in text for w in _exclkeywords)):
284 or not ui.verbose and any(w in text for w in _exclkeywords)):
285 continue
285 continue
286 text = gettext(text)
286 text = gettext(text)
287 if dedent:
287 if dedent:
288 # Abuse latin1 to use textwrap.dedent() on bytes.
288 # Abuse latin1 to use textwrap.dedent() on bytes.
289 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
289 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
290 lines = text.splitlines()
290 lines = text.splitlines()
291 doclines = [(lines[0])]
291 doclines = [(lines[0])]
292 for l in lines[1:]:
292 for l in lines[1:]:
293 # Stop once we find some Python doctest
293 # Stop once we find some Python doctest
294 if l.strip().startswith('>>>'):
294 if l.strip().startswith('>>>'):
295 break
295 break
296 if dedent:
296 if dedent:
297 doclines.append(l.rstrip())
297 doclines.append(l.rstrip())
298 else:
298 else:
299 doclines.append(' ' + l.strip())
299 doclines.append(' ' + l.strip())
300 entries.append('\n'.join(doclines))
300 entries.append('\n'.join(doclines))
301 entries = '\n\n'.join(entries)
301 entries = '\n\n'.join(entries)
302 return doc.replace(marker, entries)
302 return doc.replace(marker, entries)
303
303
304 def addtopicsymbols(topic, marker, symbols, dedent=False):
304 def addtopicsymbols(topic, marker, symbols, dedent=False):
305 def add(ui, topic, doc):
305 def add(ui, topic, doc):
306 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
306 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
307 addtopichook(topic, add)
307 addtopichook(topic, add)
308
308
309 addtopicsymbols('bundlespec', '.. bundlecompressionmarker',
309 addtopicsymbols('bundlespec', '.. bundlecompressionmarker',
310 util.bundlecompressiontopics())
310 util.bundlecompressiontopics())
311 addtopicsymbols('filesets', '.. predicatesmarker', fileset.symbols)
311 addtopicsymbols('filesets', '.. predicatesmarker', fileset.symbols)
312 addtopicsymbols('merge-tools', '.. internaltoolsmarker',
312 addtopicsymbols('merge-tools', '.. internaltoolsmarker',
313 filemerge.internalsdoc)
313 filemerge.internalsdoc)
314 addtopicsymbols('revisions', '.. predicatesmarker', revset.symbols)
314 addtopicsymbols('revisions', '.. predicatesmarker', revset.symbols)
315 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
315 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
316 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
316 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
317 addtopicsymbols('templates', '.. functionsmarker', templatefuncs.funcs)
317 addtopicsymbols('templates', '.. functionsmarker', templatefuncs.funcs)
318 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
318 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
319 dedent=True)
319 dedent=True)
320
320
321 def help_(ui, commands, name, unknowncmd=False, full=True, subtopic=None,
321 def help_(ui, commands, name, unknowncmd=False, full=True, subtopic=None,
322 **opts):
322 **opts):
323 '''
323 '''
324 Generate the help for 'name' as unformatted restructured text. If
324 Generate the help for 'name' as unformatted restructured text. If
325 'name' is None, describe the commands available.
325 'name' is None, describe the commands available.
326 '''
326 '''
327
327
328 opts = pycompat.byteskwargs(opts)
328 opts = pycompat.byteskwargs(opts)
329
329
330 def helpcmd(name, subtopic=None):
330 def helpcmd(name, subtopic=None):
331 try:
331 try:
332 aliases, entry = cmdutil.findcmd(name, commands.table,
332 aliases, entry = cmdutil.findcmd(name, commands.table,
333 strict=unknowncmd)
333 strict=unknowncmd)
334 except error.AmbiguousCommand as inst:
334 except error.AmbiguousCommand as inst:
335 # py3k fix: except vars can't be used outside the scope of the
335 # py3k fix: except vars can't be used outside the scope of the
336 # except block, nor can be used inside a lambda. python issue4617
336 # except block, nor can be used inside a lambda. python issue4617
337 prefix = inst.args[0]
337 prefix = inst.args[0]
338 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
338 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
339 rst = helplist(select)
339 rst = helplist(select)
340 return rst
340 return rst
341
341
342 rst = []
342 rst = []
343
343
344 # check if it's an invalid alias and display its error if it is
344 # check if it's an invalid alias and display its error if it is
345 if getattr(entry[0], 'badalias', None):
345 if getattr(entry[0], 'badalias', None):
346 rst.append(entry[0].badalias + '\n')
346 rst.append(entry[0].badalias + '\n')
347 if entry[0].unknowncmd:
347 if entry[0].unknowncmd:
348 try:
348 try:
349 rst.extend(helpextcmd(entry[0].cmdname))
349 rst.extend(helpextcmd(entry[0].cmdname))
350 except error.UnknownCommand:
350 except error.UnknownCommand:
351 pass
351 pass
352 return rst
352 return rst
353
353
354 # synopsis
354 # synopsis
355 if len(entry) > 2:
355 if len(entry) > 2:
356 if entry[2].startswith('hg'):
356 if entry[2].startswith('hg'):
357 rst.append("%s\n" % entry[2])
357 rst.append("%s\n" % entry[2])
358 else:
358 else:
359 rst.append('hg %s %s\n' % (aliases[0], entry[2]))
359 rst.append('hg %s %s\n' % (aliases[0], entry[2]))
360 else:
360 else:
361 rst.append('hg %s\n' % aliases[0])
361 rst.append('hg %s\n' % aliases[0])
362 # aliases
362 # aliases
363 if full and not ui.quiet and len(aliases) > 1:
363 if full and not ui.quiet and len(aliases) > 1:
364 rst.append(_("\naliases: %s\n") % ', '.join(aliases[1:]))
364 rst.append(_("\naliases: %s\n") % ', '.join(aliases[1:]))
365 rst.append('\n')
365 rst.append('\n')
366
366
367 # description
367 # description
368 doc = gettext(pycompat.getdoc(entry[0]))
368 doc = gettext(pycompat.getdoc(entry[0]))
369 if not doc:
369 if not doc:
370 doc = _("(no help text available)")
370 doc = _("(no help text available)")
371 if util.safehasattr(entry[0], 'definition'): # aliased command
371 if util.safehasattr(entry[0], 'definition'): # aliased command
372 source = entry[0].source
372 source = entry[0].source
373 if entry[0].definition.startswith('!'): # shell alias
373 if entry[0].definition.startswith('!'): # shell alias
374 doc = (_('shell alias for: %s\n\n%s\n\ndefined by: %s\n') %
374 doc = (_('shell alias for: %s\n\n%s\n\ndefined by: %s\n') %
375 (entry[0].definition[1:], doc, source))
375 (entry[0].definition[1:], doc, source))
376 else:
376 else:
377 doc = (_('alias for: hg %s\n\n%s\n\ndefined by: %s\n') %
377 doc = (_('alias for: hg %s\n\n%s\n\ndefined by: %s\n') %
378 (entry[0].definition, doc, source))
378 (entry[0].definition, doc, source))
379 doc = doc.splitlines(True)
379 doc = doc.splitlines(True)
380 if ui.quiet or not full:
380 if ui.quiet or not full:
381 rst.append(doc[0])
381 rst.append(doc[0])
382 else:
382 else:
383 rst.extend(doc)
383 rst.extend(doc)
384 rst.append('\n')
384 rst.append('\n')
385
385
386 # check if this command shadows a non-trivial (multi-line)
386 # check if this command shadows a non-trivial (multi-line)
387 # extension help text
387 # extension help text
388 try:
388 try:
389 mod = extensions.find(name)
389 mod = extensions.find(name)
390 doc = gettext(pycompat.getdoc(mod)) or ''
390 doc = gettext(pycompat.getdoc(mod)) or ''
391 if '\n' in doc.strip():
391 if '\n' in doc.strip():
392 msg = _("(use 'hg help -e %s' to show help for "
392 msg = _("(use 'hg help -e %s' to show help for "
393 "the %s extension)") % (name, name)
393 "the %s extension)") % (name, name)
394 rst.append('\n%s\n' % msg)
394 rst.append('\n%s\n' % msg)
395 except KeyError:
395 except KeyError:
396 pass
396 pass
397
397
398 # options
398 # options
399 if not ui.quiet and entry[1]:
399 if not ui.quiet and entry[1]:
400 rst.append(optrst(_("options"), entry[1], ui.verbose))
400 rst.append(optrst(_("options"), entry[1], ui.verbose))
401
401
402 if ui.verbose:
402 if ui.verbose:
403 rst.append(optrst(_("global options"),
403 rst.append(optrst(_("global options"),
404 commands.globalopts, ui.verbose))
404 commands.globalopts, ui.verbose))
405
405
406 if not ui.verbose:
406 if not ui.verbose:
407 if not full:
407 if not full:
408 rst.append(_("\n(use 'hg %s -h' to show more help)\n")
408 rst.append(_("\n(use 'hg %s -h' to show more help)\n")
409 % name)
409 % name)
410 elif not ui.quiet:
410 elif not ui.quiet:
411 rst.append(_('\n(some details hidden, use --verbose '
411 rst.append(_('\n(some details hidden, use --verbose '
412 'to show complete help)'))
412 'to show complete help)'))
413
413
414 return rst
414 return rst
415
415
416
416
417 def helplist(select=None, **opts):
417 def helplist(select=None, **opts):
418 # list of commands
418 # list of commands
419 if name == "shortlist":
419 if name == "shortlist":
420 header = _('basic commands:\n\n')
420 header = _('basic commands:\n\n')
421 elif name == "debug":
421 elif name == "debug":
422 header = _('debug commands (internal and unsupported):\n\n')
422 header = _('debug commands (internal and unsupported):\n\n')
423 else:
423 else:
424 header = _('list of commands:\n\n')
424 header = _('list of commands:\n\n')
425
425
426 h = {}
426 h = {}
427 cmds = {}
427 cmds = {}
428 for c, e in commands.table.iteritems():
428 for c, e in commands.table.iteritems():
429 fs = cmdutil.parsealiases(c)
429 fs = cmdutil.parsealiases(c)
430 f = fs[0]
430 f = fs[0]
431 p = ''
431 p = ''
432 if c.startswith("^"):
432 if c.startswith("^"):
433 p = '^'
433 p = '^'
434 if select and not select(p + f):
434 if select and not select(p + f):
435 continue
435 continue
436 if (not select and name != 'shortlist' and
436 if (not select and name != 'shortlist' and
437 e[0].__module__ != commands.__name__):
437 e[0].__module__ != commands.__name__):
438 continue
438 continue
439 if name == "shortlist" and not p:
439 if name == "shortlist" and not p:
440 continue
440 continue
441 doc = pycompat.getdoc(e[0])
441 doc = pycompat.getdoc(e[0])
442 if filtercmd(ui, f, name, doc):
442 if filtercmd(ui, f, name, doc):
443 continue
443 continue
444 doc = gettext(doc)
444 doc = gettext(doc)
445 if not doc:
445 if not doc:
446 doc = _("(no help text available)")
446 doc = _("(no help text available)")
447 h[f] = doc.splitlines()[0].rstrip()
447 h[f] = doc.splitlines()[0].rstrip()
448 cmds[f] = '|'.join(fs)
448 cmds[f] = '|'.join(fs)
449
449
450 rst = []
450 rst = []
451 if not h:
451 if not h:
452 if not ui.quiet:
452 if not ui.quiet:
453 rst.append(_('no commands defined\n'))
453 rst.append(_('no commands defined\n'))
454 return rst
454 return rst
455
455
456 if not ui.quiet:
456 if not ui.quiet:
457 rst.append(header)
457 rst.append(header)
458 fns = sorted(h)
458 fns = sorted(h)
459 for f in fns:
459 for f in fns:
460 if ui.verbose:
460 if ui.verbose:
461 commacmds = cmds[f].replace("|",", ")
461 commacmds = cmds[f].replace("|",", ")
462 rst.append(" :%s: %s\n" % (commacmds, h[f]))
462 rst.append(" :%s: %s\n" % (commacmds, h[f]))
463 else:
463 else:
464 rst.append(' :%s: %s\n' % (f, h[f]))
464 rst.append(' :%s: %s\n' % (f, h[f]))
465
465
466 ex = opts.get
466 ex = opts.get
467 anyopts = (ex(r'keyword') or not (ex(r'command') or ex(r'extension')))
467 anyopts = (ex(r'keyword') or not (ex(r'command') or ex(r'extension')))
468 if not name and anyopts:
468 if not name and anyopts:
469 exts = listexts(_('enabled extensions:'), extensions.enabled())
469 exts = listexts(_('enabled extensions:'), extensions.enabled())
470 if exts:
470 if exts:
471 rst.append('\n')
471 rst.append('\n')
472 rst.extend(exts)
472 rst.extend(exts)
473
473
474 rst.append(_("\nadditional help topics:\n\n"))
474 rst.append(_("\nadditional help topics:\n\n"))
475 topics = []
475 topics = []
476 for names, header, doc in helptable:
476 for names, header, doc in helptable:
477 topics.append((names[0], header))
477 topics.append((names[0], header))
478 for t, desc in topics:
478 for t, desc in topics:
479 rst.append(" :%s: %s\n" % (t, desc))
479 rst.append(" :%s: %s\n" % (t, desc))
480
480
481 if ui.quiet:
481 if ui.quiet:
482 pass
482 pass
483 elif ui.verbose:
483 elif ui.verbose:
484 rst.append('\n%s\n' % optrst(_("global options"),
484 rst.append('\n%s\n' % optrst(_("global options"),
485 commands.globalopts, ui.verbose))
485 commands.globalopts, ui.verbose))
486 if name == 'shortlist':
486 if name == 'shortlist':
487 rst.append(_("\n(use 'hg help' for the full list "
487 rst.append(_("\n(use 'hg help' for the full list "
488 "of commands)\n"))
488 "of commands)\n"))
489 else:
489 else:
490 if name == 'shortlist':
490 if name == 'shortlist':
491 rst.append(_("\n(use 'hg help' for the full list of commands "
491 rst.append(_("\n(use 'hg help' for the full list of commands "
492 "or 'hg -v' for details)\n"))
492 "or 'hg -v' for details)\n"))
493 elif name and not full:
493 elif name and not full:
494 rst.append(_("\n(use 'hg help %s' to show the full help "
494 rst.append(_("\n(use 'hg help %s' to show the full help "
495 "text)\n") % name)
495 "text)\n") % name)
496 elif name and cmds and name in cmds.keys():
496 elif name and cmds and name in cmds.keys():
497 rst.append(_("\n(use 'hg help -v -e %s' to show built-in "
497 rst.append(_("\n(use 'hg help -v -e %s' to show built-in "
498 "aliases and global options)\n") % name)
498 "aliases and global options)\n") % name)
499 else:
499 else:
500 rst.append(_("\n(use 'hg help -v%s' to show built-in aliases "
500 rst.append(_("\n(use 'hg help -v%s' to show built-in aliases "
501 "and global options)\n")
501 "and global options)\n")
502 % (name and " " + name or ""))
502 % (name and " " + name or ""))
503 return rst
503 return rst
504
504
505 def helptopic(name, subtopic=None):
505 def helptopic(name, subtopic=None):
506 # Look for sub-topic entry first.
506 # Look for sub-topic entry first.
507 header, doc = None, None
507 header, doc = None, None
508 if subtopic and name in subtopics:
508 if subtopic and name in subtopics:
509 for names, header, doc in subtopics[name]:
509 for names, header, doc in subtopics[name]:
510 if subtopic in names:
510 if subtopic in names:
511 break
511 break
512
512
513 if not header:
513 if not header:
514 for names, header, doc in helptable:
514 for names, header, doc in helptable:
515 if name in names:
515 if name in names:
516 break
516 break
517 else:
517 else:
518 raise error.UnknownCommand(name)
518 raise error.UnknownCommand(name)
519
519
520 rst = [minirst.section(header)]
520 rst = [minirst.section(header)]
521
521
522 # description
522 # description
523 if not doc:
523 if not doc:
524 rst.append(" %s\n" % _("(no help text available)"))
524 rst.append(" %s\n" % _("(no help text available)"))
525 if callable(doc):
525 if callable(doc):
526 rst += [" %s\n" % l for l in doc(ui).splitlines()]
526 rst += [" %s\n" % l for l in doc(ui).splitlines()]
527
527
528 if not ui.verbose:
528 if not ui.verbose:
529 omitted = _('(some details hidden, use --verbose'
529 omitted = _('(some details hidden, use --verbose'
530 ' to show complete help)')
530 ' to show complete help)')
531 indicateomitted(rst, omitted)
531 indicateomitted(rst, omitted)
532
532
533 try:
533 try:
534 cmdutil.findcmd(name, commands.table)
534 cmdutil.findcmd(name, commands.table)
535 rst.append(_("\nuse 'hg help -c %s' to see help for "
535 rst.append(_("\nuse 'hg help -c %s' to see help for "
536 "the %s command\n") % (name, name))
536 "the %s command\n") % (name, name))
537 except error.UnknownCommand:
537 except error.UnknownCommand:
538 pass
538 pass
539 return rst
539 return rst
540
540
541 def helpext(name, subtopic=None):
541 def helpext(name, subtopic=None):
542 try:
542 try:
543 mod = extensions.find(name)
543 mod = extensions.find(name)
544 doc = gettext(pycompat.getdoc(mod)) or _('no help text available')
544 doc = gettext(pycompat.getdoc(mod)) or _('no help text available')
545 except KeyError:
545 except KeyError:
546 mod = None
546 mod = None
547 doc = extensions.disabledext(name)
547 doc = extensions.disabledext(name)
548 if not doc:
548 if not doc:
549 raise error.UnknownCommand(name)
549 raise error.UnknownCommand(name)
550
550
551 if '\n' not in doc:
551 if '\n' not in doc:
552 head, tail = doc, ""
552 head, tail = doc, ""
553 else:
553 else:
554 head, tail = doc.split('\n', 1)
554 head, tail = doc.split('\n', 1)
555 rst = [_('%s extension - %s\n\n') % (name.rpartition('.')[-1], head)]
555 rst = [_('%s extension - %s\n\n') % (name.rpartition('.')[-1], head)]
556 if tail:
556 if tail:
557 rst.extend(tail.splitlines(True))
557 rst.extend(tail.splitlines(True))
558 rst.append('\n')
558 rst.append('\n')
559
559
560 if not ui.verbose:
560 if not ui.verbose:
561 omitted = _('(some details hidden, use --verbose'
561 omitted = _('(some details hidden, use --verbose'
562 ' to show complete help)')
562 ' to show complete help)')
563 indicateomitted(rst, omitted)
563 indicateomitted(rst, omitted)
564
564
565 if mod:
565 if mod:
566 try:
566 try:
567 ct = mod.cmdtable
567 ct = mod.cmdtable
568 except AttributeError:
568 except AttributeError:
569 ct = {}
569 ct = {}
570 modcmds = set([c.partition('|')[0] for c in ct])
570 modcmds = set([c.partition('|')[0] for c in ct])
571 rst.extend(helplist(modcmds.__contains__))
571 rst.extend(helplist(modcmds.__contains__))
572 else:
572 else:
573 rst.append(_("(use 'hg help extensions' for information on enabling"
573 rst.append(_("(use 'hg help extensions' for information on enabling"
574 " extensions)\n"))
574 " extensions)\n"))
575 return rst
575 return rst
576
576
577 def helpextcmd(name, subtopic=None):
577 def helpextcmd(name, subtopic=None):
578 cmd, ext, doc = extensions.disabledcmd(ui, name,
578 cmd, ext, doc = extensions.disabledcmd(ui, name,
579 ui.configbool('ui', 'strict'))
579 ui.configbool('ui', 'strict'))
580 doc = doc.splitlines()[0]
580 doc = doc.splitlines()[0]
581
581
582 rst = listexts(_("'%s' is provided by the following "
582 rst = listexts(_("'%s' is provided by the following "
583 "extension:") % cmd, {ext: doc}, indent=4,
583 "extension:") % cmd, {ext: doc}, indent=4,
584 showdeprecated=True)
584 showdeprecated=True)
585 rst.append('\n')
585 rst.append('\n')
586 rst.append(_("(use 'hg help extensions' for information on enabling "
586 rst.append(_("(use 'hg help extensions' for information on enabling "
587 "extensions)\n"))
587 "extensions)\n"))
588 return rst
588 return rst
589
589
590
590
591 rst = []
591 rst = []
592 kw = opts.get('keyword')
592 kw = opts.get('keyword')
593 if kw or name is None and any(opts[o] for o in opts):
593 if kw or name is None and any(opts[o] for o in opts):
594 matches = topicmatch(ui, commands, name or '')
594 matches = topicmatch(ui, commands, name or '')
595 helpareas = []
595 helpareas = []
596 if opts.get('extension'):
596 if opts.get('extension'):
597 helpareas += [('extensions', _('Extensions'))]
597 helpareas += [('extensions', _('Extensions'))]
598 if opts.get('command'):
598 if opts.get('command'):
599 helpareas += [('commands', _('Commands'))]
599 helpareas += [('commands', _('Commands'))]
600 if not helpareas:
600 if not helpareas:
601 helpareas = [('topics', _('Topics')),
601 helpareas = [('topics', _('Topics')),
602 ('commands', _('Commands')),
602 ('commands', _('Commands')),
603 ('extensions', _('Extensions')),
603 ('extensions', _('Extensions')),
604 ('extensioncommands', _('Extension Commands'))]
604 ('extensioncommands', _('Extension Commands'))]
605 for t, title in helpareas:
605 for t, title in helpareas:
606 if matches[t]:
606 if matches[t]:
607 rst.append('%s:\n\n' % title)
607 rst.append('%s:\n\n' % title)
608 rst.extend(minirst.maketable(sorted(matches[t]), 1))
608 rst.extend(minirst.maketable(sorted(matches[t]), 1))
609 rst.append('\n')
609 rst.append('\n')
610 if not rst:
610 if not rst:
611 msg = _('no matches')
611 msg = _('no matches')
612 hint = _("try 'hg help' for a list of topics")
612 hint = _("try 'hg help' for a list of topics")
613 raise error.Abort(msg, hint=hint)
613 raise error.Abort(msg, hint=hint)
614 elif name and name != 'shortlist':
614 elif name and name != 'shortlist':
615 queries = []
615 queries = []
616 if unknowncmd:
616 if unknowncmd:
617 queries += [helpextcmd]
617 queries += [helpextcmd]
618 if opts.get('extension'):
618 if opts.get('extension'):
619 queries += [helpext]
619 queries += [helpext]
620 if opts.get('command'):
620 if opts.get('command'):
621 queries += [helpcmd]
621 queries += [helpcmd]
622 if not queries:
622 if not queries:
623 queries = (helptopic, helpcmd, helpext, helpextcmd)
623 queries = (helptopic, helpcmd, helpext, helpextcmd)
624 for f in queries:
624 for f in queries:
625 try:
625 try:
626 rst = f(name, subtopic)
626 rst = f(name, subtopic)
627 break
627 break
628 except error.UnknownCommand:
628 except error.UnknownCommand:
629 pass
629 pass
630 else:
630 else:
631 if unknowncmd:
631 if unknowncmd:
632 raise error.UnknownCommand(name)
632 raise error.UnknownCommand(name)
633 else:
633 else:
634 msg = _('no such help topic: %s') % name
634 msg = _('no such help topic: %s') % name
635 hint = _("try 'hg help --keyword %s'") % name
635 hint = _("try 'hg help --keyword %s'") % name
636 raise error.Abort(msg, hint=hint)
636 raise error.Abort(msg, hint=hint)
637 else:
637 else:
638 # program name
638 # program name
639 if not ui.quiet:
639 if not ui.quiet:
640 rst = [_("Mercurial Distributed SCM\n"), '\n']
640 rst = [_("Mercurial Distributed SCM\n"), '\n']
641 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
641 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
642
642
643 return ''.join(rst)
643 return ''.join(rst)
644
644
645 def formattedhelp(ui, commands, name, keep=None, unknowncmd=False, full=True,
645 def formattedhelp(ui, commands, name, keep=None, unknowncmd=False, full=True,
646 **opts):
646 **opts):
647 """get help for a given topic (as a dotted name) as rendered rst
647 """get help for a given topic (as a dotted name) as rendered rst
648
648
649 Either returns the rendered help text or raises an exception.
649 Either returns the rendered help text or raises an exception.
650 """
650 """
651 if keep is None:
651 if keep is None:
652 keep = []
652 keep = []
653 else:
653 else:
654 keep = list(keep) # make a copy so we can mutate this later
654 keep = list(keep) # make a copy so we can mutate this later
655 fullname = name
655 fullname = name
656 section = None
656 section = None
657 subtopic = None
657 subtopic = None
658 if name and '.' in name:
658 if name and '.' in name:
659 name, remaining = name.split('.', 1)
659 name, remaining = name.split('.', 1)
660 remaining = encoding.lower(remaining)
660 remaining = encoding.lower(remaining)
661 if '.' in remaining:
661 if '.' in remaining:
662 subtopic, section = remaining.split('.', 1)
662 subtopic, section = remaining.split('.', 1)
663 else:
663 else:
664 if name in subtopics:
664 if name in subtopics:
665 subtopic = remaining
665 subtopic = remaining
666 else:
666 else:
667 section = remaining
667 section = remaining
668 textwidth = ui.configint('ui', 'textwidth')
668 textwidth = ui.configint('ui', 'textwidth')
669 termwidth = ui.termwidth() - 2
669 termwidth = ui.termwidth() - 2
670 if textwidth <= 0 or termwidth < textwidth:
670 if textwidth <= 0 or termwidth < textwidth:
671 textwidth = termwidth
671 textwidth = termwidth
672 text = help_(ui, commands, name,
672 text = help_(ui, commands, name,
673 subtopic=subtopic, unknowncmd=unknowncmd, full=full, **opts)
673 subtopic=subtopic, unknowncmd=unknowncmd, full=full, **opts)
674
674
675 blocks, pruned = minirst.parse(text, keep=keep)
675 blocks, pruned = minirst.parse(text, keep=keep)
676 if section:
677 blocks = minirst.filtersections(blocks, section)
678 formatted = minirst.formatplain(blocks, textwidth)
679
680 # We could have been given a weird ".foo" section without a name
681 # to look for, or we could have simply failed to found "foo.bar"
682 # because bar isn't a section of foo
683 if section and not (formatted and name):
684 raise error.Abort(_("help section not found: %s") % fullname)
685
686 if 'verbose' in pruned:
676 if 'verbose' in pruned:
687 keep.append('omitted')
677 keep.append('omitted')
688 else:
678 else:
689 keep.append('notomitted')
679 keep.append('notomitted')
690 blocks, pruned = minirst.parse(text, keep=keep)
680 blocks, pruned = minirst.parse(text, keep=keep)
691 if section:
681 if section:
692 blocks = minirst.filtersections(blocks, section)
682 blocks = minirst.filtersections(blocks, section)
683
684 # We could have been given a weird ".foo" section without a name
685 # to look for, or we could have simply failed to found "foo.bar"
686 # because bar isn't a section of foo
687 if section and not (blocks and name):
688 raise error.Abort(_("help section not found: %s") % fullname)
689
693 return minirst.formatplain(blocks, textwidth)
690 return minirst.formatplain(blocks, textwidth)
General Comments 0
You need to be logged in to leave comments. Login now