classifier of glossary terms can be used for index entries grouping key. The classifier also be used for translation. See also :ref:glossary-directive.

This commit is contained in:
shimizukawa 2016-02-13 22:30:06 +09:00
parent 358a13ee72
commit e6a5a3a92e
20 changed files with 111 additions and 65 deletions

View File

@ -73,6 +73,8 @@ Features added
sections using its title. Thanks to Tadhg O'Higgins. sections using its title. Thanks to Tadhg O'Higgins.
* #1854: Allow to choose Janome for Japanese splitter. * #1854: Allow to choose Janome for Japanese splitter.
* #1853: support custom text splitter on html search with `language='ja'`. * #1853: support custom text splitter on html search with `language='ja'`.
* #2320: classifier of glossary terms can be used for index entries grouping key.
The classifier also be used for translation. See also :ref:`glossary-directive`.
Bugs fixed Bugs fixed
---------- ----------

View File

@ -183,6 +183,24 @@ Glossary
(When the glossary is sorted, the first term determines the sort order.) (When the glossary is sorted, the first term determines the sort order.)
If you want to specify "grouping key" for general index entries, you can put a "key"
as "term : key". For example::
.. glossary::
term 1 : A
term 2 : B
Definition of both terms.
Note that "key" is used for grouping key as is.
The "key" isn't normalized; key "A" and "a" become different groups.
The whole characters in "key" is used instead of a first character; it is used for
"Combining Character Sequence" and "Surrogate Pairs" grouping key.
In i18n situation, you can specify "localized term : key" even if original text only
have "term" part. In this case, translated "localized term" will be categorized in
"key" group.
.. versionadded:: 0.6 .. versionadded:: 0.6
You can now give the glossary directive a ``:sorted:`` flag that will You can now give the glossary directive a ``:sorted:`` flag that will
automatically sort the entries alphabetically. automatically sort the entries alphabetically.
@ -190,6 +208,8 @@ Glossary
.. versionchanged:: 1.1 .. versionchanged:: 1.1
Now supports multiple terms and inline markup in terms. Now supports multiple terms and inline markup in terms.
.. versionchanged:: 1.4
Index key for glossary term should be considered *experimental*.
Grammar production displays Grammar production displays
--------------------------- ---------------------------

View File

@ -400,7 +400,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
# XXX: modifies tree inline # XXX: modifies tree inline
# Logic modeled from themes/basic/genindex.html # Logic modeled from themes/basic/genindex.html
for key, columns in tree: for key, columns in tree:
for entryname, (links, subitems) in columns: for entryname, (links, subitems, key_) in columns:
for (i, (ismain, link)) in enumerate(links): for (i, (ismain, link)) in enumerate(links):
m = self.refuri_re.match(link) m = self.refuri_re.match(link)
if m: if m:

View File

@ -117,7 +117,7 @@ class I18nBuilder(Builder):
if 'index' in self.env.config.gettext_additional_targets: if 'index' in self.env.config.gettext_additional_targets:
# Extract translatable messages from index entries. # Extract translatable messages from index entries.
for node, entries in traverse_translatable_index(doctree): for node, entries in traverse_translatable_index(doctree):
for typ, msg, tid, main in entries: for typ, msg, tid, main, key_ in entries:
for m in split_index_msg(typ, msg): for m in split_index_msg(typ, msg):
if typ == 'pair' and m in pairindextypes.values(): if typ == 'pair' and m in pairindextypes.values():
# avoid built-in translated message was incorporated # avoid built-in translated message was incorporated

View File

@ -512,7 +512,7 @@ class StandaloneHTMLBuilder(Builder):
indexcounts = [] indexcounts = []
for _k, entries in genindex: for _k, entries in genindex:
indexcounts.append(sum(1 + len(subitems) indexcounts.append(sum(1 + len(subitems)
for _, (_, subitems) in entries)) for _, (_, subitems, _) in entries))
genindexcontext = dict( genindexcontext = dict(
genindexentries = genindex, genindexentries = genindex,

View File

@ -299,7 +299,7 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder):
write_index(subitem[0], subitem[1], []) write_index(subitem[0], subitem[1], [])
f.write('</UL>') f.write('</UL>')
for (key, group) in index: for (key, group) in index:
for title, (refs, subitems) in group: for title, (refs, subitems, key_) in group:
write_index(title, refs, subitems) write_index(title, refs, subitems)
f.write('</UL>\n') f.write('</UL>\n')
finally: finally:

View File

@ -148,7 +148,7 @@ class QtHelpBuilder(StandaloneHTMLBuilder):
keywords = [] keywords = []
index = self.env.create_index(self, group_entries=False) index = self.env.create_index(self, group_entries=False)
for (key, group) in index: for (key, group) in index:
for title, (refs, subitems) in group: for title, (refs, subitems, key_) in group:
keywords.extend(self.build_keywords(title, refs, subitems)) keywords.extend(self.build_keywords(title, refs, subitems))
keywords = u'\n'.join(keywords) keywords = u'\n'.join(keywords)

View File

@ -206,7 +206,7 @@ class CObject(ObjectDescription):
indextext = self.get_index_text(name) indextext = self.get_index_text(name)
if indextext: if indextext:
self.indexnode['entries'].append(('single', indextext, self.indexnode['entries'].append(('single', indextext,
targetname, '')) targetname, '', None))
def before_content(self): def before_content(self):
self.typename_set = False self.typename_set = False

View File

@ -3677,7 +3677,7 @@ class CPPObject(ObjectDescription):
name = text_type(ast.symbol.get_full_nested_name()).lstrip(':') name = text_type(ast.symbol.get_full_nested_name()).lstrip(':')
indexText = self.get_index_text(name) indexText = self.get_index_text(name)
self.indexnode['entries'].append(('single', indexText, newestId, '')) self.indexnode['entries'].append(('single', indexText, newestId, '', None))
if newestId not in self.state.document.ids: if newestId not in self.state.document.ids:
# if the name is not unique, the first one will win # if the name is not unique, the first one will win

View File

@ -97,7 +97,7 @@ class JSObject(ObjectDescription):
if indextext: if indextext:
self.indexnode['entries'].append(('single', indextext, self.indexnode['entries'].append(('single', indextext,
fullname.replace('$', '_S_'), fullname.replace('$', '_S_'),
'')) '', None))
def get_index_text(self, objectname, name_obj): def get_index_text(self, objectname, name_obj):
name, obj = name_obj name, obj = name_obj

View File

@ -257,7 +257,7 @@ class PyObject(ObjectDescription):
indextext = self.get_index_text(modname, name_cls) indextext = self.get_index_text(modname, name_cls)
if indextext: if indextext:
self.indexnode['entries'].append(('single', indextext, self.indexnode['entries'].append(('single', indextext,
fullname, '')) fullname, '', None))
def before_content(self): def before_content(self):
# needed for automatic qualification of members (reset in subclasses) # needed for automatic qualification of members (reset in subclasses)
@ -462,7 +462,7 @@ class PyModule(Directive):
ret.append(targetnode) ret.append(targetnode)
indextext = _('%s (module)') % modname indextext = _('%s (module)') % modname
inode = addnodes.index(entries=[('single', indextext, inode = addnodes.index(entries=[('single', indextext,
'module-' + modname, '')]) 'module-' + modname, '', None)])
ret.append(inode) ret.append(inode)
return ret return ret

View File

@ -48,7 +48,7 @@ class ReSTMarkup(ObjectDescription):
indextext = self.get_index_text(self.objtype, name) indextext = self.get_index_text(self.objtype, name)
if indextext: if indextext:
self.indexnode['entries'].append(('single', indextext, self.indexnode['entries'].append(('single', indextext,
targetname, '')) targetname, '', None))
def get_index_text(self, objectname, name): def get_index_text(self, objectname, name):
if self.objtype == 'directive': if self.objtype == 'directive':

View File

@ -64,7 +64,7 @@ class GenericObject(ObjectDescription):
indextype = 'single' indextype = 'single'
indexentry = self.indextemplate % (name,) indexentry = self.indextemplate % (name,)
self.indexnode['entries'].append((indextype, indexentry, self.indexnode['entries'].append((indextype, indexentry,
targetname, '')) targetname, '', None))
self.env.domaindata['std']['objects'][self.objtype, name] = \ self.env.domaindata['std']['objects'][self.objtype, name] = \
self.env.docname, targetname self.env.docname, targetname
@ -85,8 +85,8 @@ class EnvVarXRefRole(XRefRole):
tgtid = 'index-%s' % env.new_serialno('index') tgtid = 'index-%s' % env.new_serialno('index')
indexnode = addnodes.index() indexnode = addnodes.index()
indexnode['entries'] = [ indexnode['entries'] = [
('single', varname, tgtid, ''), ('single', varname, tgtid, '', None),
('single', _('environment variable; %s') % varname, tgtid, '') ('single', _('environment variable; %s') % varname, tgtid, '', None)
] ]
targetnode = nodes.target('', '', ids=[tgtid]) targetnode = nodes.target('', '', ids=[tgtid])
document.note_explicit_target(targetnode) document.note_explicit_target(targetnode)
@ -184,7 +184,7 @@ class Cmdoption(ObjectDescription):
self.indexnode['entries'].append( self.indexnode['entries'].append(
('pair', _('%scommand line option; %s') % ('pair', _('%scommand line option; %s') %
((currprogram and currprogram + ' ' or ''), sig), ((currprogram and currprogram + ' ' or ''), sig),
targetname, '')) targetname, '', None))
class Program(Directive): class Program(Directive):
@ -214,11 +214,23 @@ class OptionXRefRole(XRefRole):
return title, target return title, target
def register_term_to_glossary(env, node, new_id=None): def split_term_classifiers(line):
# split line into a term and classifiers. if no classifier, None is used..
parts = re.split(' +: +', line) + [None]
return parts
def make_glossary_term(env, textnodes, index_key, source, lineno, new_id=None):
# get a text-only representation of the term and register it
# as a cross-reference target
term = nodes.term('', '', *textnodes)
term.source = source
term.line = lineno
gloss_entries = env.temp_data.setdefault('gloss_entries', set()) gloss_entries = env.temp_data.setdefault('gloss_entries', set())
objects = env.domaindata['std']['objects'] objects = env.domaindata['std']['objects']
termtext = node.astext() termtext = term.astext()
if new_id is None: if new_id is None:
new_id = nodes.make_id('term-' + termtext) new_id = nodes.make_id('term-' + termtext)
if new_id in gloss_entries: if new_id in gloss_entries:
@ -228,11 +240,13 @@ def register_term_to_glossary(env, node, new_id=None):
# add an index entry too # add an index entry too
indexnode = addnodes.index() indexnode = addnodes.index()
indexnode['entries'] = [('single', termtext, new_id, 'main')] indexnode['entries'] = [('single', termtext, new_id, 'main', index_key)]
indexnode.source, indexnode.line = node.source, node.line indexnode.source, indexnode.line = term.source, term.line
node.append(indexnode) term.append(indexnode)
node['ids'].append(new_id) term['ids'].append(new_id)
node['names'].append(new_id) term['names'].append(new_id)
return term
class Glossary(Directive): class Glossary(Directive):
@ -316,16 +330,15 @@ class Glossary(Directive):
termnodes = [] termnodes = []
system_messages = [] system_messages = []
for line, source, lineno in terms: for line, source, lineno in terms:
parts = split_term_classifiers(line)
# parse the term with inline markup # parse the term with inline markup
res = self.state.inline_text(line, lineno) # classifiers (parts[1:]) will not be shown on doctree
system_messages.extend(res[1]) textnodes, sysmsg = self.state.inline_text(parts[0], lineno)
# get a text-only representation of the term and register it # use first classifier as a index key
# as a cross-reference target term = make_glossary_term(env, textnodes, parts[1], source, lineno)
term = nodes.term('', '', *res[0]) term.rawsource = line
term.source = source system_messages.extend(sysmsg)
term.line = lineno
register_term_to_glossary(env, term)
termtexts.append(term.astext()) termtexts.append(term.astext())
termnodes.append(term) termnodes.append(term)

View File

@ -1049,7 +1049,7 @@ class BuildEnvironment:
entries = self.indexentries[docname] = [] entries = self.indexentries[docname] = []
for node in document.traverse(addnodes.index): for node in document.traverse(addnodes.index):
try: try:
for type, value, tid, main in node['entries']: for type, value, tid, main, index_key in node['entries']:
split_index_msg(type, value) split_index_msg(type, value)
except ValueError as exc: except ValueError as exc:
self.warn_node(exc, node) self.warn_node(exc, node)
@ -1771,16 +1771,16 @@ class BuildEnvironment:
"""Create the real index from the collected index entries.""" """Create the real index from the collected index entries."""
new = {} new = {}
def add_entry(word, subword, link=True, dic=new): def add_entry(word, subword, link=True, dic=new, key=None):
# Force the word to be unicode if it's a ASCII bytestring. # Force the word to be unicode if it's a ASCII bytestring.
# This will solve problems with unicode normalization later. # This will solve problems with unicode normalization later.
# For instance the RFC role will add bytestrings at the moment # For instance the RFC role will add bytestrings at the moment
word = text_type(word) word = text_type(word)
entry = dic.get(word) entry = dic.get(word)
if not entry: if not entry:
dic[word] = entry = [[], {}] dic[word] = entry = [[], {}, key]
if subword: if subword:
add_entry(subword, '', link=link, dic=entry[1]) add_entry(subword, '', link=link, dic=entry[1], key=key)
elif link: elif link:
try: try:
uri = builder.get_relative_uri('genindex', fn) + '#' + tid uri = builder.get_relative_uri('genindex', fn) + '#' + tid
@ -1792,7 +1792,7 @@ class BuildEnvironment:
for fn, entries in iteritems(self.indexentries): for fn, entries in iteritems(self.indexentries):
# new entry types must be listed in directives/other.py! # new entry types must be listed in directives/other.py!
for type, value, tid, main in entries: for type, value, tid, main, index_key in entries:
try: try:
if type == 'single': if type == 'single':
try: try:
@ -1800,22 +1800,24 @@ class BuildEnvironment:
except ValueError: except ValueError:
entry, = split_into(1, 'single', value) entry, = split_into(1, 'single', value)
subentry = '' subentry = ''
add_entry(entry, subentry) add_entry(entry, subentry, key=index_key)
elif type == 'pair': elif type == 'pair':
first, second = split_into(2, 'pair', value) first, second = split_into(2, 'pair', value)
add_entry(first, second) add_entry(first, second, key=index_key)
add_entry(second, first) add_entry(second, first, key=index_key)
elif type == 'triple': elif type == 'triple':
first, second, third = split_into(3, 'triple', value) first, second, third = split_into(3, 'triple', value)
add_entry(first, second+' '+third) add_entry(first, second+' '+third, key=index_key)
add_entry(second, third+', '+first) add_entry(second, third+', '+first, key=index_key)
add_entry(third, first+' '+second) add_entry(third, first+' '+second, key=index_key)
elif type == 'see': elif type == 'see':
first, second = split_into(2, 'see', value) first, second = split_into(2, 'see', value)
add_entry(first, _('see %s') % second, link=False) add_entry(first, _('see %s') % second, link=False,
key=index_key)
elif type == 'seealso': elif type == 'seealso':
first, second = split_into(2, 'see', value) first, second = split_into(2, 'see', value)
add_entry(first, _('see also %s') % second, link=False) add_entry(first, _('see also %s') % second, link=False,
key=index_key)
else: else:
self.warn(fn, 'unknown index entry type %r' % type) self.warn(fn, 'unknown index entry type %r' % type)
except ValueError as err: except ValueError as err:
@ -1844,7 +1846,7 @@ class BuildEnvironment:
oldsubitems = None oldsubitems = None
i = 0 i = 0
while i < len(newlist): while i < len(newlist):
key, (targets, subitems) = newlist[i] key, (targets, subitems, _key) = newlist[i]
# cannot move if it has subitems; structure gets too complex # cannot move if it has subitems; structure gets too complex
if not subitems: if not subitems:
m = _fixre.match(key) m = _fixre.match(key)
@ -1852,7 +1854,7 @@ class BuildEnvironment:
if oldkey == m.group(1): if oldkey == m.group(1):
# prefixes match: add entry as subitem of the # prefixes match: add entry as subitem of the
# previous entry # previous entry
oldsubitems.setdefault(m.group(2), [[], {}])[0].\ oldsubitems.setdefault(m.group(2), [[], {}, _key])[0].\
extend(targets) extend(targets)
del newlist[i] del newlist[i]
continue continue
@ -1866,7 +1868,8 @@ class BuildEnvironment:
def keyfunc2(item, letters=string.ascii_uppercase + '_'): def keyfunc2(item, letters=string.ascii_uppercase + '_'):
# hack: mutating the subitems dicts to a list in the keyfunc # hack: mutating the subitems dicts to a list in the keyfunc
k, v = item k, v = item
v[1] = sorted((si, se) for (si, (se, void)) in iteritems(v[1])) v[1] = sorted((si, se) for (si, (se, void, void)) in iteritems(v[1]))
if v[2] is None:
# now calculate the key # now calculate the key
letter = unicodedata.normalize('NFD', k[0])[0].upper() letter = unicodedata.normalize('NFD', k[0])[0].upper()
if letter in letters: if letter in letters:
@ -1874,6 +1877,8 @@ class BuildEnvironment:
else: else:
# get all other symbols under one heading # get all other symbols under one heading
return _('Symbols') return _('Symbols')
else:
return v[2]
return [(key_, list(group)) return [(key_, list(group))
for (key_, group) in groupby(newlist, keyfunc2)] for (key_, group) in groupby(newlist, keyfunc2)]

View File

@ -191,7 +191,7 @@ def indexmarkup_role(typ, rawtext, text, lineno, inliner,
if typ == 'pep': if typ == 'pep':
indexnode['entries'] = [ indexnode['entries'] = [
('single', _('Python Enhancement Proposals; PEP %s') % target, ('single', _('Python Enhancement Proposals; PEP %s') % target,
targetid, '')] targetid, '', None)]
anchor = '' anchor = ''
anchorindex = target.find('#') anchorindex = target.find('#')
if anchorindex > 0: if anchorindex > 0:
@ -212,7 +212,8 @@ def indexmarkup_role(typ, rawtext, text, lineno, inliner,
rn += sn rn += sn
return [indexnode, targetnode, rn], [] return [indexnode, targetnode, rn], []
elif typ == 'rfc': elif typ == 'rfc':
indexnode['entries'] = [('single', 'RFC; RFC %s' % target, targetid, '')] indexnode['entries'] = [
('single', 'RFC; RFC %s' % target, targetid, '', None)]
anchor = '' anchor = ''
anchorindex = target.find('#') anchorindex = target.find('#')
if anchorindex > 0: if anchorindex > 0:
@ -317,7 +318,7 @@ def index_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
target = target[1:] target = target[1:]
title = title[1:] title = title[1:]
main = 'main' main = 'main'
entries = [('single', target, targetid, main)] entries = [('single', target, targetid, main, None)]
indexnode = addnodes.index() indexnode = addnodes.index()
indexnode['entries'] = entries indexnode['entries'] = entries
set_role_source_info(inliner, lineno, indexnode) set_role_source_info(inliner, lineno, indexnode)

View File

@ -46,7 +46,7 @@
<table style="width: 100%" class="indextable genindextable"><tr> <table style="width: 100%" class="indextable genindextable"><tr>
{%- for column in entries|slice(2) if column %} {%- for column in entries|slice(2) if column %}
<td style="width: 33%" valign="top"><dl> <td style="width: 33%" valign="top"><dl>
{%- for entryname, (links, subitems) in column %} {%- for entryname, (links, subitems, _) in column %}
{{ indexentries(entryname, links) }} {{ indexentries(entryname, links) }}
{%- if subitems %} {%- if subitems %}
<dd><dl> <dd><dl>

View File

@ -27,7 +27,7 @@ from sphinx.util.nodes import (
from sphinx.util.osutil import ustrftime from sphinx.util.osutil import ustrftime
from sphinx.util.i18n import find_catalog from sphinx.util.i18n import find_catalog
from sphinx.util.pycompat import indent from sphinx.util.pycompat import indent
from sphinx.domains.std import register_term_to_glossary from sphinx.domains.std import make_glossary_term, split_term_classifiers
default_substitutions = set([ default_substitutions = set([
@ -340,7 +340,12 @@ class Locale(Transform):
for _id in node['names']: for _id in node['names']:
if _id in gloss_entries: if _id in gloss_entries:
gloss_entries.remove(_id) gloss_entries.remove(_id)
register_term_to_glossary(env, patch, _id)
parts = split_term_classifiers(msgstr)
patch = publish_msgstr(
env.app, parts[0], source, node.line, env.config, settings)
patch = make_glossary_term(
env, patch, parts[1], source, node.line, _id)
node['ids'] = patch['ids'] node['ids'] = patch['ids']
node['names'] = patch['names'] node['names'] = patch['names']
processed = True processed = True
@ -521,7 +526,7 @@ class Locale(Transform):
# Extract and translate messages for index entries. # Extract and translate messages for index entries.
for node, entries in traverse_translatable_index(self.document): for node, entries in traverse_translatable_index(self.document):
new_entries = [] new_entries = []
for type, msg, tid, main in entries: for type, msg, tid, main, key_ in entries:
msg_parts = split_index_msg(type, msg) msg_parts = split_index_msg(type, msg)
msgstr_parts = [] msgstr_parts = []
for part in msg_parts: for part in msg_parts:
@ -530,7 +535,7 @@ class Locale(Transform):
msgstr = part msgstr = part
msgstr_parts.append(msgstr) msgstr_parts.append(msgstr)
new_entries.append((type, ';'.join(msgstr_parts), tid, main)) new_entries.append((type, ';'.join(msgstr_parts), tid, main, None))
node['raw_entries'] = entries node['raw_entries'] = entries
node['entries'] = new_entries node['entries'] = new_entries

View File

@ -213,7 +213,7 @@ def process_index_entry(entry, targetid):
if entry.startswith(type+':'): if entry.startswith(type+':'):
value = entry[len(type)+1:].strip() value = entry[len(type)+1:].strip()
value = pairindextypes[type] + '; ' + value value = pairindextypes[type] + '; ' + value
indexentries.append(('pair', value, targetid, main)) indexentries.append(('pair', value, targetid, main, None))
break break
else: else:
for type in indextypes: for type in indextypes:
@ -221,7 +221,7 @@ def process_index_entry(entry, targetid):
value = entry[len(type)+1:].strip() value = entry[len(type)+1:].strip()
if type == 'double': if type == 'double':
type = 'pair' type = 'pair'
indexentries.append((type, value, targetid, main)) indexentries.append((type, value, targetid, main, None))
break break
# shorthand notation for single entries # shorthand notation for single entries
else: else:
@ -233,7 +233,7 @@ def process_index_entry(entry, targetid):
value = value[1:].lstrip() value = value[1:].lstrip()
if not value: if not value:
continue continue
indexentries.append(('single', value, targetid, main)) indexentries.append(('single', value, targetid, main, None))
return indexentries return indexentries

View File

@ -1531,7 +1531,7 @@ class LaTeXTranslator(nodes.NodeVisitor):
if not node.get('inline', True): if not node.get('inline', True):
self.body.append('\n') self.body.append('\n')
entries = node['entries'] entries = node['entries']
for type, string, tid, ismain in entries: for type, string, tid, ismain, key_ in entries:
m = '' m = ''
if ismain: if ismain:
m = '|textbf' m = '|textbf'

View File

@ -1302,7 +1302,7 @@ class TexinfoTranslator(nodes.NodeVisitor):
else: else:
self.body.append('\n') self.body.append('\n')
for entry in node['entries']: for entry in node['entries']:
typ, text, tid, text2 = entry typ, text, tid, text2, key_ = entry
text = self.escape_menu(text) text = self.escape_menu(text)
self.body.append('@geindex %s\n' % text) self.body.append('@geindex %s\n' % text)