category_meta.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. '''Copyright 2014 Zack Weinberg
  2. Category Metadata
  3. -----------------
  4. A plugin to read metadata for each category from an index file in that
  5. category's directory.
  6. For this plugin to work properly, your articles should not have a
  7. Category: tag in their metadata; instead, they should be stored in
  8. (subdirectories of) per-category directories. Each per-category
  9. directory must have a file named 'index.ext' at its top level, where
  10. .ext is any extension that will be picked up by an article reader.
  11. The metadata of that article becomes the metadata for the category,
  12. copied over verbatim, with three special cases:
  13. * The category's name is set to the article's title.
  14. * The category's slug is set to the name of the parent directory
  15. of the index.ext file.
  16. * The _text_ of the article is stored as category.description.
  17. '''
  18. from pelican import signals
  19. import os
  20. import re
  21. import logging
  22. logger = logging.getLogger(__name__)
  23. ### CORE BUG: Content.url_format does not honor category.slug (or
  24. ### author.slug). The sanest way to fix this without modifying core
  25. ### is to dynamically redefine each article's class to a subclass
  26. ### of itself with the bug fixed.
  27. ###
  28. ### https://github.com/getpelican/pelican/issues/1547
  29. patched_subclasses = {}
  30. def make_patched_subclass(klass):
  31. if klass.__name__ not in patched_subclasses:
  32. class PatchedContent(klass):
  33. @property
  34. def url_format(self):
  35. metadata = super(PatchedContent, self).url_format
  36. if hasattr(self.author, 'slug'):
  37. metadata['author'] = self.author.slug
  38. if hasattr(self.category, 'slug'):
  39. metadata['category'] = self.category.slug
  40. return metadata
  41. # Code in core uses Content class names as keys for things.
  42. PatchedContent.__name__ = klass.__name__
  43. patched_subclasses[klass.__name__] = PatchedContent
  44. return patched_subclasses[klass.__name__]
  45. def patch_urlformat(article):
  46. article.__class__ = make_patched_subclass(article.__class__)
  47. def make_category(article, slug):
  48. # Reuse the article's existing category object.
  49. category = article.category
  50. # Setting a category's name resets its slug, so do that first.
  51. category.name = article.title
  52. category.slug = slug
  53. # Description from article text.
  54. # XXX Relative URLs in the article content may not be handled correctly.
  55. setattr(category, 'description', article.content)
  56. # Metadata, to the extent that this makes sense.
  57. for k, v in article.metadata.items():
  58. if k not in ('path', 'slug', 'category', 'name', 'title',
  59. 'description', 'reader'):
  60. setattr(category, k, v)
  61. logger.debug("Category: %s -> %s", category.slug, category.name)
  62. return category
  63. def pretaxonomy_hook(generator):
  64. """This hook is invoked before the generator's .categories property is
  65. filled in. Each article has already been assigned a category
  66. object, but these objects are _not_ unique per category and so are
  67. not safe to tack metadata onto (as is).
  68. The category metadata we're looking for is represented as an
  69. Article object, one per directory, whose filename is 'index.ext'.
  70. """
  71. category_objects = {}
  72. real_articles = []
  73. for article in generator.articles:
  74. dirname, fname = os.path.split(article.source_path)
  75. fname, _ = os.path.splitext(fname)
  76. if fname == 'index':
  77. category_objects[dirname] = \
  78. make_category(article, os.path.basename(dirname))
  79. else:
  80. real_articles.append(article)
  81. category_assignment = \
  82. re.compile("^(" +
  83. "|".join(re.escape(prefix)
  84. for prefix in category_objects.keys()) +
  85. ")/")
  86. for article in real_articles:
  87. m = category_assignment.match(article.source_path)
  88. if not m or m.group(1) not in category_objects:
  89. logger.error("No category assignment for %s (%s)",
  90. article, article.source_path)
  91. continue
  92. patch_urlformat(article)
  93. article.category = category_objects[m.group(1)]
  94. generator.articles = real_articles
  95. def register():
  96. signals.article_generator_pretaxonomy.connect(pretaxonomy_hook)