PageRenderTime 119ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/pelican-toc/toc.py

https://gitlab.com/janninematt/janninematt
Python | 145 lines | 115 code | 21 blank | 9 comment | 5 complexity | c1630e8354d88d9449a50466a6d8fec3 MD5 | raw file
  1. '''
  2. toc
  3. ===================================
  4. This plugin generates tocs for pages and articles.
  5. '''
  6. from __future__ import unicode_literals
  7. import logging
  8. import re
  9. from bs4 import BeautifulSoup, Comment
  10. from pelican import contents, signals
  11. from pelican.utils import python_2_unicode_compatible, slugify
  12. logger = logging.getLogger(__name__)
  13. '''
  14. https://github.com/waylan/Python-Markdown/blob/master/markdown/extensions/headerid.py
  15. '''
  16. IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$')
  17. def unique(id, ids):
  18. """ Ensure id is unique in set of ids. Append '_1', '_2'... if not """
  19. while id in ids or not id:
  20. m = IDCOUNT_RE.match(id)
  21. if m:
  22. id = '%s_%d' % (m.group(1), int(m.group(2)) + 1)
  23. else:
  24. id = '%s_%d' % (id, 1)
  25. ids.add(id)
  26. return id
  27. '''
  28. end
  29. '''
  30. @python_2_unicode_compatible
  31. class HtmlTreeNode(object):
  32. def __init__(self, parent, header, level, id):
  33. self.children = []
  34. self.parent = parent
  35. self.header = header
  36. self.level = level
  37. self.id = id
  38. def add(self, new_header, ids):
  39. new_level = new_header.name
  40. new_string = new_header.string
  41. new_id = new_header.attrs.get('id')
  42. if not new_string:
  43. new_string = new_header.find_all(
  44. text=lambda t: not isinstance(t, Comment),
  45. recursive=True)
  46. new_string = "".join(new_string)
  47. if not new_id:
  48. new_id = slugify(new_string, ())
  49. new_id = unique(new_id, ids) # make sure id is unique
  50. new_header.attrs['id'] = new_id
  51. if(self.level < new_level):
  52. new_node = HtmlTreeNode(self, new_string, new_level, new_id)
  53. self.children += [new_node]
  54. return new_node, new_header
  55. elif(self.level == new_level):
  56. new_node = HtmlTreeNode(self.parent, new_string, new_level, new_id)
  57. self.parent.children += [new_node]
  58. return new_node, new_header
  59. elif(self.level > new_level):
  60. return self.parent.add(new_header, ids)
  61. def __str__(self):
  62. ret = "<a class='toc-href' href='#{0}' title='{1}'>{1}</a>".format(
  63. self.id, self.header)
  64. if self.children:
  65. ret += "<ul>{}</ul>".format('{}'*len(self.children)).format(
  66. *self.children)
  67. ret = "<li>{}</li>".format(ret)
  68. if not self.parent:
  69. ret = "<div id='toc'><ul>{}</ul></div>".format(ret)
  70. return ret
  71. def init_default_config(pelican):
  72. from pelican.settings import DEFAULT_CONFIG
  73. TOC_DEFAULT = {
  74. 'TOC_HEADERS': '^h[1-6]',
  75. 'TOC_RUN': 'true'
  76. }
  77. DEFAULT_CONFIG.setdefault('TOC', TOC_DEFAULT)
  78. if(pelican):
  79. pelican.settings.setdefault('TOC', TOC_DEFAULT)
  80. def generate_toc(content):
  81. if isinstance(content, contents.Static):
  82. return
  83. _toc_run = content.metadata.get(
  84. 'toc_run',
  85. content.settings['TOC']['TOC_RUN'])
  86. if not _toc_run == 'true':
  87. return
  88. all_ids = set()
  89. title = content.metadata.get('title', 'Title')
  90. tree = node = HtmlTreeNode(None, title, 'h0', '')
  91. soup = BeautifulSoup(content._content, 'html.parser')
  92. settoc = False
  93. try:
  94. header_re = re.compile(content.metadata.get(
  95. 'toc_headers', content.settings['TOC']['TOC_HEADERS']))
  96. except re.error as e:
  97. logger.error("TOC_HEADERS '%s' is not a valid re\n%s",
  98. content.settings['TOC']['TOC_HEADERS'])
  99. raise e
  100. for header in soup.findAll(header_re):
  101. settoc = True
  102. node, new_header = node.add(header, all_ids)
  103. header.replaceWith(new_header) # to get our ids back into soup
  104. if (settoc):
  105. tree_string = '{}'.format(tree)
  106. tree_soup = BeautifulSoup(tree_string, 'html.parser')
  107. content.toc = tree_soup.decode(formatter='html')
  108. content._content = soup.decode(formatter='html')
  109. def register():
  110. signals.initialized.connect(init_default_config)
  111. signals.content_object_init.connect(generate_toc)