Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a parent directive to @section #97

Merged
merged 7 commits into from
Jan 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,13 @@ Available block directives include:
- `@private` marks a function private.
- `@section name[, id]` allows you to write a new section for the helpfile. The
id will be a lowercased version of name if omitted.
- `@parentsection id` defines the current section as a child of the given
section. Must be contained within a `@section` block.
- `@subsection name` defines a subsection (heading) within a section block.
- `@backmatter id` declares a block to be rendered at the end of the given
section.
- `@order ...` allows you to define the order of the sections.
- `@order ...` allows you to define the order of the sections. Sections with a
`@parentsection` may not be included here.
- `@dict name` (above blank lines) allows you to define a new dictionary.
- `@dict dict.fn` (above a function) allows you to add a function to
a dictionary.
Expand Down
58 changes: 58 additions & 0 deletions tests/module_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,64 @@ def test_manual_section_ordering(self):
main_module.Close()
self.assertEqual([commands, about, intro], list(main_module.Chunks()))

def test_child_sections(self):
"""Sections should be ordered after their parents."""
plugin = module.VimPlugin('myplugin')
main_module = module.Module('myplugin', plugin)
first = Block(vimdoc.SECTION)
first.Local(name='Section 1', id='first')
# Configure explicit order.
first.Global(order=['first', 'second', 'third'])
second = Block(vimdoc.SECTION)
second.Local(name='Section 2', id='second')
third = Block(vimdoc.SECTION)
third.Local(name='Section 3', id='third')
child11 = Block(vimdoc.SECTION)
child11.Local(name='child11', id='child11', parent_id='first')
child12 = Block(vimdoc.SECTION)
child12.Local(name='child12', id='child12', parent_id='first')
child21 = Block(vimdoc.SECTION)
child21.Local(name='child21', id='child21', parent_id='second')
# Merge in arbitrary order.
for m in [second, child12, third, child11, first, child21]:
main_module.Merge(m)
main_module.Close()
self.assertEqual(
[first, child11, child12, second, child21, third],
list(main_module.Chunks()))

def test_missing_parent(self):
"""Parent sections should exist."""
plugin = module.VimPlugin('myplugin')
main_module = module.Module('myplugin', plugin)
first = Block(vimdoc.SECTION)
first.Local(name='Section 1', id='first')
second = Block(vimdoc.SECTION)
second.Local(name='Section 2', id='second', parent_id='missing')
main_module.Merge(first)
main_module.Merge(second)
with self.assertRaises(error.NoSuchParentSection) as cm:
main_module.Close()
expected = (
'Section Section 2 has non-existent parent missing. '
'Try setting the id of the parent section explicitly.')
self.assertEqual((expected,), cm.exception.args)

def test_ordered_child(self):
"""Child sections should not be included in @order."""
plugin = module.VimPlugin('myplugin')
main_module = module.Module('myplugin', plugin)
first = Block(vimdoc.SECTION)
first.Local(name='Section 1', id='first')
second = Block(vimdoc.SECTION)
second.Local(name='Section 2', id='second', parent_id='first')
first.Global(order=['first', 'second'])
main_module.Merge(first)
main_module.Merge(second)
with self.assertRaises(error.OrderedChildSections) as cm:
main_module.Close()
self.assertEqual(("Child section second included in ordering ['first', 'second'].",), cm.exception.args)

def test_partial_ordering(self):
"""Always respect explicit order and prefer built-in ordering.

Expand Down
9 changes: 9 additions & 0 deletions vimdoc/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def __init__(self, type=None, is_secondary=False, is_default=False):
# name (of section)
# type (constant, e.g. vimdoc.FUNCTION)
# id (of section, in section or backmatter)
# parent_id (in section)
# children (in section)
# level (in section, tracks nesting level)
# namespace (of function)
# attribute (of function in dict)
self.locals = {}
Expand Down Expand Up @@ -137,6 +140,12 @@ def SetType(self, newtype):
else:
raise error.TypeConflict(ourtype, newtype)

def SetParentSection(self, parent_id):
"""Sets the parent_id for blocks of type SECTION"""
if not (self.locals.get('type') == vimdoc.SECTION):
raise error.MisplacedParentSection(parent_id)
self.Local(parent_id=parent_id)

def SetHeader(self, directive):
"""Sets the header handler."""
if self.header:
Expand Down
15 changes: 15 additions & 0 deletions vimdoc/docline.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,12 @@ def Update(self, block):
class Section(BlockDirective):
REGEX = regex.section_args

def __init__(self, args):
super(Section, self).__init__(args)

def Assign(self, name, ident):
self.name = name.replace('\\,', ',').replace('\\\\', '\\')

if ident is None:
# If omitted, it's the name in lowercase, with spaces converted to dashes.
ident = self.name.lower().replace(' ', '-')
Expand All @@ -192,6 +196,16 @@ def Update(self, block):
block.Local(name=self.name, id=self.id)


class ParentSection(BlockDirective):
REGEX = regex.parent_section_args

def Assign(self, name):
self.name = name.lower()

def Update(self, block):
block.SetParentSection(self.name)


class Setting(BlockDirective):
REGEX = regex.one_arg

Expand Down Expand Up @@ -384,6 +398,7 @@ def GenerateUsage(self, block):
'function': Function,
'library': Library,
'order': Order,
'parentsection': ParentSection,
'private': Private,
'public': Public,
'section': Section,
Expand Down
20 changes: 20 additions & 0 deletions vimdoc/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ def __init__(self, section):
'Section {} never defined.'.format(section))


class MisplacedParentSection(BadStructure):
def __init__(self, section):
super(MisplacedParentSection, self).__init__(
'Parent section {} defined outside a @section block.'.format(section))


class NoSuchParentSection(BadStructure):
def __init__(self, section, parent_id):
super(NoSuchParentSection, self).__init__(
('Section {} has non-existent parent {}. '
'Try setting the id of the parent section explicitly.'
).format(section, parent_id))


class DuplicateSection(BadStructure):
def __init__(self, section):
super(DuplicateSection, self).__init__(
Expand All @@ -132,3 +146,9 @@ class NeglectedSections(BadStructure):
def __init__(self, sections, order):
super(NeglectedSections, self).__init__(
'Sections {} not included in ordering {}.'.format(sections, order))


class OrderedChildSections(BadStructure):
def __init__(self, section, order):
super(OrderedChildSections, self).__init__(
'Child section {} included in ordering {}.'.format(section, order))
42 changes: 40 additions & 2 deletions vimdoc/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,57 @@ def Close(self):
for backmatter in self.backmatters:
if backmatter not in self.sections:
raise error.NoSuchSection(backmatter)

# Use explicit order as partial ordering and merge with default section
# ordering. All custom sections must be ordered explicitly.
self.order = self._GetSectionOrder(self.order, self.sections)

# Child section collection
to_delete = []
for key in self.sections:
section = self.sections[key]
parent_id = section.locals.get('parent_id', None)
if parent_id:
if parent_id not in self.sections:
raise error.NoSuchParentSection(
section.locals['name'], parent_id)
parent = self.sections[parent_id]
parent.locals.setdefault('children', []).append(section)
to_delete.append(key)

for key in to_delete:
self.sections.pop(key)

known = set(self.sections)
neglected = sorted(known.difference(self.order))
if neglected:
raise error.NeglectedSections(neglected, self.order)
# Sections are now in order.

# Reinsert top-level sections in the correct order, expanding the tree of
# child sections along the way so we have a linear list of sections to pass
# to the output functions.

# Helper function to recursively add children to self.sections.
# We add a 'level' variable to locals so that WriteTableOfContents can keep
# track of the nesting.
def _AddChildSections(section):
section.locals.setdefault('level', 0)
if 'children' in section.locals:
sort_key = lambda s: s.locals['name']
for child in sorted(section.locals['children'], key=sort_key):
child.locals['level'] = section.locals['level'] + 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to use += 1 here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they're two different objects :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
Oh yeah, so they are…

self.sections[child.locals['id']] = child
_AddChildSections(child)

# Insert sections according to the @order directive
for key in self.order:
if key in self.sections:
section = self.sections.pop(key)
if 'parent_id' in section.locals and section.locals['parent_id']:
raise error.OrderedChildSections(section.locals['id'], self.order)
# Move to end.
self.sections[key] = self.sections.pop(key)
self.sections[key] = section
_AddChildSections(section)

def Chunks(self):
for ident, section in self.sections.items():
Expand Down
29 changes: 23 additions & 6 deletions vimdoc/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,33 @@ def WriteHeader(self):
self.WriteLine(right=tag)
self.WriteLine()

# Helper function for WriteTableOfContents().
def _EnumerateIndices(self, sections):
"""Keep track of section numbering for each level of the tree"""
count = [{'level': 0, 'index': 0}]
for block in sections:
assert 'id' in block.locals
assert 'name' in block.locals
level = block.locals['level']
while level < count[-1]['level']:
count.pop()
if level == count[-1]['level']:
count[-1]['index'] += 1
else:
count.append({'level': level, 'index': 1})
yield (count[-1]['index'], block)

def WriteTableOfContents(self):
"""Writes the table of contents."""
self.WriteRow()
self.WriteLine('CONTENTS', right=self.Tag(self.Slug('contents')))
for i, block in enumerate(self.module.sections.values()):
assert 'id' in block.locals
assert 'name' in block.locals
line = '%d. %s' % (i + 1, block.locals['name'])
slug = self.Slug(block.locals['id'])
self.WriteLine(line, indent=1, right=self.Link(slug), fill='.')
# We need to keep track of section numbering on a per-level basis
for index, block in self._EnumerateIndices(self.module.sections.values()):
self.WriteLine(
'%d. %s' % (index, block.locals['name']),
indent=2 * block.locals['level'] + 1,
right=self.Link(self.Slug(block.locals['id'])),
fill='.')
self.WriteLine()

def WriteChunk(self, chunk):
Expand Down
8 changes: 7 additions & 1 deletion vimdoc/regex.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
>>> section_args.match('The Beginning, beg').groups()
('The Beginning', 'beg')

>>> parent_section_args.match('123')
>>> parent_section_args.match('foo').groups()
('foo',)

>>> backmatter_args.match('123')
>>> backmatter_args.match('foo').groups()
('foo',)
Expand Down Expand Up @@ -211,7 +215,8 @@ def _DelimitedRegex(pattern):
# MATCH GROUP 1: The Name
(
# Non-commas or escaped commas or escaped escapes.
(?:[^\\,]|\\.)+
# Must not end with a space.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this change leftover from the other approach, or valuable in itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valuable in itself - it prevents problems with trailing spaces being changed to dashes.

(?:[^\\,]|\\.)+\S
)
# Optional identifier
(?:
Expand All @@ -222,6 +227,7 @@ def _DelimitedRegex(pattern):
)?
$
""", re.VERBOSE)
parent_section_args = re.compile(r'([a-zA-Z_-][a-zA-Z0-9_-]*)')
backmatter_args = re.compile(r'([a-zA-Z_-][a-zA-Z0-9_-]*)')
dict_args = re.compile(r"""
^([a-zA-Z_][a-zA-Z0-9]*)(?:\.([a-zA-Z_][a-zA-Z0-9_]*))?$
Expand Down