We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
{category} in URLs isn't slugified
When trying to save articles using:

```
CATEGORY_SAVE_AS = "{slug}/index.html" 
ARTICLE_SAVE_AS = "{category}/{slug}/index.html"
```

the `{category}` in the second instance isn't slugified, so the URLs don't line up neatly and may end up with spaces or other URL-unfriendly characters in them. Ditto `{author}`.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pelican/contents.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals, print_function
3 import six
4 
5 import copy
6 import locale
7 import logging
8 import functools
9 import os
10 import re
11 import sys
12 
13 from datetime import datetime
14 
15 
16 from pelican import signals
17 from pelican.settings import DEFAULT_CONFIG
18 from pelican.utils import (slugify, truncate_html_words, memoized, strftime,
19                            python_2_unicode_compatible, deprecated_attribute,
20                            path_to_url)
21 
22 # Import these so that they're avalaible when you import from pelican.contents.
23 from pelican.urlwrappers import (URLWrapper, Author, Category, Tag)  # NOQA
24 
25 logger = logging.getLogger(__name__)
26 
27 
28 class Content(object):
29     """Represents a content.
30 
31     :param content: the string to parse, containing the original content.
32     :param metadata: the metadata associated to this page (optional).
33     :param settings: the settings dictionary (optional).
34     :param source_path: The location of the source of this content (if any).
35     :param context: The shared context between generators.
36 
37     """
38     @deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))
39     def filename():
40         return None
41 
42     def __init__(self, content, metadata=None, settings=None,
43                  source_path=None, context=None):
44         if metadata is None:
45             metadata = {}
46         if settings is None:
47             settings = copy.deepcopy(DEFAULT_CONFIG)
48 
49         self.settings = settings
50         self._content = content
51         if context is None:
52             context = {}
53         self._context = context
54         self.translations = []
55 
56         local_metadata = dict(settings['DEFAULT_METADATA'])
57         local_metadata.update(metadata)
58 
59         # set metadata as attributes
60         for key, value in local_metadata.items():
61             if key in ('save_as', 'url'):
62                 key = 'override_' + key
63             setattr(self, key.lower(), value)
64 
65         # also keep track of the metadata attributes available
66         self.metadata = local_metadata
67 
68         #default template if it's not defined in page
69         self.template = self._get_template()
70 
71         # default author to the one in settings if not defined
72         if not hasattr(self, 'author'):
73             if 'AUTHOR' in settings:
74                 self.author = Author(settings['AUTHOR'], settings)
75 
76         # XXX Split all the following code into pieces, there is too much here.
77 
78         # manage languages
79         self.in_default_lang = True
80         if 'DEFAULT_LANG' in settings:
81             default_lang = settings['DEFAULT_LANG'].lower()
82             if not hasattr(self, 'lang'):
83                 self.lang = default_lang
84 
85             self.in_default_lang = (self.lang == default_lang)
86 
87         # create the slug if not existing, from the title
88         if not hasattr(self, 'slug') and hasattr(self, 'title'):
89             self.slug = slugify(self.title,
90                                 settings.get('SLUG_SUBSTITUTIONS', ()))
91 
92         self.source_path = source_path
93 
94         # manage the date format
95         if not hasattr(self, 'date_format'):
96             if hasattr(self, 'lang') and self.lang in settings['DATE_FORMATS']:
97                 self.date_format = settings['DATE_FORMATS'][self.lang]
98             else:
99                 self.date_format = settings['DEFAULT_DATE_FORMAT']
100 
101         if isinstance(self.date_format, tuple):
102             locale_string = self.date_format[0]
103             if sys.version_info < (3, ) and isinstance(locale_string,
104                                                        six.text_type):
105                 locale_string = locale_string.encode('ascii')
106             locale.setlocale(locale.LC_ALL, locale_string)
107             self.date_format = self.date_format[1]
108 
109         if hasattr(self, 'date'):
110             self.locale_date = strftime(self.date, self.date_format)
111 
112         # manage status
113         if not hasattr(self, 'status'):
114             self.status = settings['DEFAULT_STATUS']
115             if not settings['WITH_FUTURE_DATES']:
116                 if hasattr(self, 'date') and self.date > datetime.now():
117                     self.status = 'draft'
118 
119         # store the summary metadata if it is set
120         if 'summary' in metadata:
121             self._summary = metadata['summary']
122 
123         signals.content_object_init.send(self)
124 
125     def __str__(self):
126         if self.source_path is None:
127             return repr(self)
128         elif six.PY3:
129             return self.source_path or repr(self)
130         else:
131             return str(self.source_path.encode('utf-8', 'replace'))
132 
133     def check_properties(self):
134         """Test mandatory properties are set."""
135         for prop in self.mandatory_properties:
136             if not hasattr(self, prop):
137                 raise NameError(prop)
138 
139     @property
140     def url_format(self):
141         """Returns the URL, formatted with the proper values"""
142         metadata = copy.copy(self.metadata)
143         path = self.metadata.get('path', self.get_relative_source_path())
144         metadata.update({
145             'path': path_to_url(path),
146             'slug': getattr(self, 'slug', ''),
147             'lang': getattr(self, 'lang', 'en'),
148             'date': getattr(self, 'date', datetime.now()),
149             'author': getattr(self, 'author', ''),
150             'category': getattr(self, 'category',
151                                 self.settings['DEFAULT_CATEGORY']),
152         })
153         return metadata
154 
155     def _expand_settings(self, key):
156         fq_key = ('%s_%s' % (self.__class__.__name__, key)).upper()
157         return self.settings[fq_key].format(**self.url_format)
158 
159     def get_url_setting(self, key):
160         if hasattr(self, 'override_' + key):
161             return getattr(self, 'override_' + key)
162         key = key if self.in_default_lang else 'lang_%s' % key
163         return self._expand_settings(key)
164 
165     def _update_content(self, content, siteurl):
166         """Update the content attribute.
167 
168         Change all the relative paths of the content to relative paths
169         suitable for the ouput content.
170 
171         :param content: content resource that will be passed to the templates.
172         :param siteurl: siteurl which is locally generated by the writer in
173                         case of RELATIVE_URLS.
174         """
175         if not content:
176             return content
177 
178         hrefs = re.compile(r"""
179             (?P<markup><\s*[^\>]*  # match tag with src and href attr
180                 (?:href|src)\s*=)
181 
182             (?P<quote>["\'])      # require value to be quoted
183             (?P<path>\|(?P<what>.*?)\|(?P<value>.*?))  # the url value
184             \2""", re.X)
185 
186         def replacer(m):
187             what = m.group('what')
188             value = m.group('value')
189             origin = m.group('path')
190 
191             # we support only filename for now. the plan is to support
192             # categories, tags, etc. in the future, but let's keep things
193             # simple for now.
194 
195             # XXX Put this in a different location.
196             if what == 'filename':
197                 if value.startswith('/'):
198                     value = value[1:]
199                 else:
200                     # relative to the source path of this content
201                     value = self.get_relative_source_path(
202                         os.path.join(self.relative_dir, value)
203                     )
204 
205                 if value in self._context['filenames']:
206                     origin = '/'.join((siteurl,
207                              self._context['filenames'][value].url))
208                     origin = origin.replace('\\', '/')  # Fow windows paths.
209                 else:
210                     logger.warning("Unable to find {fn}, skipping url"
211                                    " replacement".format(fn=value))
212 
213             return ''.join((m.group('markup'), m.group('quote'), origin,
214                             m.group('quote')))
215 
216         return hrefs.sub(replacer, content)
217 
218     @memoized
219     def get_content(self, siteurl):
220 
221         if hasattr(self, '_get_content'):
222             content = self._get_content()
223         else:
224             content = self._content
225         return self._update_content(content, siteurl)
226 
227     @property
228     def content(self):
229         return self.get_content(self._context.get('localsiteurl', ''))
230 
231     def _get_summary(self):
232         """Returns the summary of an article.
233 
234         This is based on the summary metadata if set, otherwise truncate the
235         content.
236         """
237         if hasattr(self, '_summary'):
238             return self._summary
239 
240         if self.settings['SUMMARY_MAX_LENGTH'] is None:
241             return self.content
242 
243         return truncate_html_words(self.content,
244                                    self.settings['SUMMARY_MAX_LENGTH'])
245 
246     def _set_summary(self, summary):
247         """Dummy function"""
248         pass
249 
250     summary = property(_get_summary, _set_summary, "Summary of the article."
251                        "Based on the content. Can't be set")
252     url = property(functools.partial(get_url_setting, key='url'))
253     save_as = property(functools.partial(get_url_setting, key='save_as'))
254 
255     def _get_template(self):
256         if hasattr(self, 'template') and self.template is not None:
257             return self.template
258         else:
259             return self.default_template
260 
261     def get_relative_source_path(self, source_path=None):
262         """Return the relative path (from the content path) to the given
263         source_path.
264 
265         If no source path is specified, use the source path of this
266         content object.
267         """
268         if not source_path:
269             source_path = self.source_path
270         if source_path is None:
271             return None
272 
273         return os.path.relpath(
274             os.path.abspath(os.path.join(self.settings['PATH'], source_path)),
275             os.path.abspath(self.settings['PATH'])
276         )
277 
278     @property
279     def relative_dir(self):
280         return os.path.dirname(os.path.relpath(
281             os.path.abspath(self.source_path),
282             os.path.abspath(self.settings['PATH']))
283         )
284 
285 
286 class Page(Content):
287     mandatory_properties = ('title',)
288     default_template = 'page'
289 
290 
291 class Article(Page):
292     mandatory_properties = ('title', 'date', 'category')
293     default_template = 'article'
294 
295 
296 class Quote(Page):
297     base_properties = ('author', 'date')
298 
299 
300 @python_2_unicode_compatible
301 class Static(Page):
302     @deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))
303     def filepath():
304         return None
305 
306     @deprecated_attribute(old='src', new='source_path', since=(3, 2, 0))
307     def src():
308         return None
309 
310     @deprecated_attribute(old='dst', new='save_as', since=(3, 2, 0))
311     def dst():
312         return None
313 
314 
315 def is_valid_content(content, f):
316     try:
317         content.check_properties()
318         return True
319     except NameError as e:
320         logger.error("Skipping %s: could not find information about "
321                      "'%s'" % (f, e))
322         return False
323
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/pelican/contents.py b/pelican/contents.py
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -141,14 +141,21 @@
         """Returns the URL, formatted with the proper values"""
         metadata = copy.copy(self.metadata)
         path = self.metadata.get('path', self.get_relative_source_path())
+        default_category = self.settings['DEFAULT_CATEGORY']
+        slug_substitutions = self.settings.get('SLUG_SUBSTITUTIONS', ())
         metadata.update({
             'path': path_to_url(path),
             'slug': getattr(self, 'slug', ''),
             'lang': getattr(self, 'lang', 'en'),
             'date': getattr(self, 'date', datetime.now()),
-            'author': getattr(self, 'author', ''),
-            'category': getattr(self, 'category',
-                                self.settings['DEFAULT_CATEGORY']),
+            'author': slugify(
+                getattr(self, 'author', ''),
+                slug_substitutions
+            ),
+            'category': slugify(
+                getattr(self, 'category', default_category),
+                slug_substitutions
+            )
         })
         return metadata

{"golden_diff": "diff --git a/pelican/contents.py b/pelican/contents.py\n--- a/pelican/contents.py\n+++ b/pelican/contents.py\n@@ -141,14 +141,21 @@\n         \"\"\"Returns the URL, formatted with the proper values\"\"\"\n         metadata = copy.copy(self.metadata)\n         path = self.metadata.get('path', self.get_relative_source_path())\n+        default_category = self.settings['DEFAULT_CATEGORY']\n+        slug_substitutions = self.settings.get('SLUG_SUBSTITUTIONS', ())\n         metadata.update({\n             'path': path_to_url(path),\n             'slug': getattr(self, 'slug', ''),\n             'lang': getattr(self, 'lang', 'en'),\n             'date': getattr(self, 'date', datetime.now()),\n-            'author': getattr(self, 'author', ''),\n-            'category': getattr(self, 'category',\n-                                self.settings['DEFAULT_CATEGORY']),\n+            'author': slugify(\n+                getattr(self, 'author', ''),\n+                slug_substitutions\n+            ),\n+            'category': slugify(\n+                getattr(self, 'category', default_category),\n+                slug_substitutions\n+            )\n         })\n         return metadata\n", "issue": "{category} in URLs isn't slugified\nWhen trying to save articles using:\n\n```\nCATEGORY_SAVE_AS = \"{slug}/index.html\" \nARTICLE_SAVE_AS = \"{category}/{slug}/index.html\"\n```\n\nthe `{category}` in the second instance isn't slugified, so the URLs don't line up neatly and may end up with spaces or other URL-unfriendly characters in them. Ditto `{author}`.\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals, print_function\nimport six\n\nimport copy\nimport locale\nimport logging\nimport functools\nimport os\nimport re\nimport sys\n\nfrom datetime import datetime\n\n\nfrom pelican import signals\nfrom pelican.settings import DEFAULT_CONFIG\nfrom pelican.utils import (slugify, truncate_html_words, memoized, strftime,\n                           python_2_unicode_compatible, deprecated_attribute,\n                           path_to_url)\n\n# Import these so that they're avalaible when you import from pelican.contents.\nfrom pelican.urlwrappers import (URLWrapper, Author, Category, Tag)  # NOQA\n\nlogger = logging.getLogger(__name__)\n\n\nclass Content(object):\n    \"\"\"Represents a content.\n\n    :param content: the string to parse, containing the original content.\n    :param metadata: the metadata associated to this page (optional).\n    :param settings: the settings dictionary (optional).\n    :param source_path: The location of the source of this content (if any).\n    :param context: The shared context between generators.\n\n    \"\"\"\n    @deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))\n    def filename():\n        return None\n\n    def __init__(self, content, metadata=None, settings=None,\n                 source_path=None, context=None):\n        if metadata is None:\n            metadata = {}\n        if settings is None:\n            settings = copy.deepcopy(DEFAULT_CONFIG)\n\n        self.settings = settings\n        self._content = content\n        if context is None:\n            context = {}\n        self._context = context\n        self.translations = []\n\n        local_metadata = dict(settings['DEFAULT_METADATA'])\n        local_metadata.update(metadata)\n\n        # set metadata as attributes\n        for key, value in local_metadata.items():\n            if key in ('save_as', 'url'):\n                key = 'override_' + key\n            setattr(self, key.lower(), value)\n\n        # also keep track of the metadata attributes available\n        self.metadata = local_metadata\n\n        #default template if it's not defined in page\n        self.template = self._get_template()\n\n        # default author to the one in settings if not defined\n        if not hasattr(self, 'author'):\n            if 'AUTHOR' in settings:\n                self.author = Author(settings['AUTHOR'], settings)\n\n        # XXX Split all the following code into pieces, there is too much here.\n\n        # manage languages\n        self.in_default_lang = True\n        if 'DEFAULT_LANG' in settings:\n            default_lang = settings['DEFAULT_LANG'].lower()\n            if not hasattr(self, 'lang'):\n                self.lang = default_lang\n\n            self.in_default_lang = (self.lang == default_lang)\n\n        # create the slug if not existing, from the title\n        if not hasattr(self, 'slug') and hasattr(self, 'title'):\n            self.slug = slugify(self.title,\n                                settings.get('SLUG_SUBSTITUTIONS', ()))\n\n        self.source_path = source_path\n\n        # manage the date format\n        if not hasattr(self, 'date_format'):\n            if hasattr(self, 'lang') and self.lang in settings['DATE_FORMATS']:\n                self.date_format = settings['DATE_FORMATS'][self.lang]\n            else:\n                self.date_format = settings['DEFAULT_DATE_FORMAT']\n\n        if isinstance(self.date_format, tuple):\n            locale_string = self.date_format[0]\n            if sys.version_info < (3, ) and isinstance(locale_string,\n                                                       six.text_type):\n                locale_string = locale_string.encode('ascii')\n            locale.setlocale(locale.LC_ALL, locale_string)\n            self.date_format = self.date_format[1]\n\n        if hasattr(self, 'date'):\n            self.locale_date = strftime(self.date, self.date_format)\n\n        # manage status\n        if not hasattr(self, 'status'):\n            self.status = settings['DEFAULT_STATUS']\n            if not settings['WITH_FUTURE_DATES']:\n                if hasattr(self, 'date') and self.date > datetime.now():\n                    self.status = 'draft'\n\n        # store the summary metadata if it is set\n        if 'summary' in metadata:\n            self._summary = metadata['summary']\n\n        signals.content_object_init.send(self)\n\n    def __str__(self):\n        if self.source_path is None:\n            return repr(self)\n        elif six.PY3:\n            return self.source_path or repr(self)\n        else:\n            return str(self.source_path.encode('utf-8', 'replace'))\n\n    def check_properties(self):\n        \"\"\"Test mandatory properties are set.\"\"\"\n        for prop in self.mandatory_properties:\n            if not hasattr(self, prop):\n                raise NameError(prop)\n\n    @property\n    def url_format(self):\n        \"\"\"Returns the URL, formatted with the proper values\"\"\"\n        metadata = copy.copy(self.metadata)\n        path = self.metadata.get('path', self.get_relative_source_path())\n        metadata.update({\n            'path': path_to_url(path),\n            'slug': getattr(self, 'slug', ''),\n            'lang': getattr(self, 'lang', 'en'),\n            'date': getattr(self, 'date', datetime.now()),\n            'author': getattr(self, 'author', ''),\n            'category': getattr(self, 'category',\n                                self.settings['DEFAULT_CATEGORY']),\n        })\n        return metadata\n\n    def _expand_settings(self, key):\n        fq_key = ('%s_%s' % (self.__class__.__name__, key)).upper()\n        return self.settings[fq_key].format(**self.url_format)\n\n    def get_url_setting(self, key):\n        if hasattr(self, 'override_' + key):\n            return getattr(self, 'override_' + key)\n        key = key if self.in_default_lang else 'lang_%s' % key\n        return self._expand_settings(key)\n\n    def _update_content(self, content, siteurl):\n        \"\"\"Update the content attribute.\n\n        Change all the relative paths of the content to relative paths\n        suitable for the ouput content.\n\n        :param content: content resource that will be passed to the templates.\n        :param siteurl: siteurl which is locally generated by the writer in\n                        case of RELATIVE_URLS.\n        \"\"\"\n        if not content:\n            return content\n\n        hrefs = re.compile(r\"\"\"\n            (?P<markup><\\s*[^\\>]*  # match tag with src and href attr\n                (?:href|src)\\s*=)\n\n            (?P<quote>[\"\\'])      # require value to be quoted\n            (?P<path>\\|(?P<what>.*?)\\|(?P<value>.*?))  # the url value\n            \\2\"\"\", re.X)\n\n        def replacer(m):\n            what = m.group('what')\n            value = m.group('value')\n            origin = m.group('path')\n\n            # we support only filename for now. the plan is to support\n            # categories, tags, etc. in the future, but let's keep things\n            # simple for now.\n\n            # XXX Put this in a different location.\n            if what == 'filename':\n                if value.startswith('/'):\n                    value = value[1:]\n                else:\n                    # relative to the source path of this content\n                    value = self.get_relative_source_path(\n                        os.path.join(self.relative_dir, value)\n                    )\n\n                if value in self._context['filenames']:\n                    origin = '/'.join((siteurl,\n                             self._context['filenames'][value].url))\n                    origin = origin.replace('\\\\', '/')  # Fow windows paths.\n                else:\n                    logger.warning(\"Unable to find {fn}, skipping url\"\n                                   \" replacement\".format(fn=value))\n\n            return ''.join((m.group('markup'), m.group('quote'), origin,\n                            m.group('quote')))\n\n        return hrefs.sub(replacer, content)\n\n    @memoized\n    def get_content(self, siteurl):\n\n        if hasattr(self, '_get_content'):\n            content = self._get_content()\n        else:\n            content = self._content\n        return self._update_content(content, siteurl)\n\n    @property\n    def content(self):\n        return self.get_content(self._context.get('localsiteurl', ''))\n\n    def _get_summary(self):\n        \"\"\"Returns the summary of an article.\n\n        This is based on the summary metadata if set, otherwise truncate the\n        content.\n        \"\"\"\n        if hasattr(self, '_summary'):\n            return self._summary\n\n        if self.settings['SUMMARY_MAX_LENGTH'] is None:\n            return self.content\n\n        return truncate_html_words(self.content,\n                                   self.settings['SUMMARY_MAX_LENGTH'])\n\n    def _set_summary(self, summary):\n        \"\"\"Dummy function\"\"\"\n        pass\n\n    summary = property(_get_summary, _set_summary, \"Summary of the article.\"\n                       \"Based on the content. Can't be set\")\n    url = property(functools.partial(get_url_setting, key='url'))\n    save_as = property(functools.partial(get_url_setting, key='save_as'))\n\n    def _get_template(self):\n        if hasattr(self, 'template') and self.template is not None:\n            return self.template\n        else:\n            return self.default_template\n\n    def get_relative_source_path(self, source_path=None):\n        \"\"\"Return the relative path (from the content path) to the given\n        source_path.\n\n        If no source path is specified, use the source path of this\n        content object.\n        \"\"\"\n        if not source_path:\n            source_path = self.source_path\n        if source_path is None:\n            return None\n\n        return os.path.relpath(\n            os.path.abspath(os.path.join(self.settings['PATH'], source_path)),\n            os.path.abspath(self.settings['PATH'])\n        )\n\n    @property\n    def relative_dir(self):\n        return os.path.dirname(os.path.relpath(\n            os.path.abspath(self.source_path),\n            os.path.abspath(self.settings['PATH']))\n        )\n\n\nclass Page(Content):\n    mandatory_properties = ('title',)\n    default_template = 'page'\n\n\nclass Article(Page):\n    mandatory_properties = ('title', 'date', 'category')\n    default_template = 'article'\n\n\nclass Quote(Page):\n    base_properties = ('author', 'date')\n\n\n@python_2_unicode_compatible\nclass Static(Page):\n    @deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))\n    def filepath():\n        return None\n\n    @deprecated_attribute(old='src', new='source_path', since=(3, 2, 0))\n    def src():\n        return None\n\n    @deprecated_attribute(old='dst', new='save_as', since=(3, 2, 0))\n    def dst():\n        return None\n\n\ndef is_valid_content(content, f):\n    try:\n        content.check_properties()\n        return True\n    except NameError as e:\n        logger.error(\"Skipping %s: could not find information about \"\n                     \"'%s'\" % (f, e))\n        return False\n", "path": "pelican/contents.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals, print_function\nimport six\n\nimport copy\nimport locale\nimport logging\nimport functools\nimport os\nimport re\nimport sys\n\nfrom datetime import datetime\n\n\nfrom pelican import signals\nfrom pelican.settings import DEFAULT_CONFIG\nfrom pelican.utils import (slugify, truncate_html_words, memoized, strftime,\n                           python_2_unicode_compatible, deprecated_attribute,\n                           path_to_url)\n\n# Import these so that they're avalaible when you import from pelican.contents.\nfrom pelican.urlwrappers import (URLWrapper, Author, Category, Tag)  # NOQA\n\nlogger = logging.getLogger(__name__)\n\n\nclass Content(object):\n    \"\"\"Represents a content.\n\n    :param content: the string to parse, containing the original content.\n    :param metadata: the metadata associated to this page (optional).\n    :param settings: the settings dictionary (optional).\n    :param source_path: The location of the source of this content (if any).\n    :param context: The shared context between generators.\n\n    \"\"\"\n    @deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))\n    def filename():\n        return None\n\n    def __init__(self, content, metadata=None, settings=None,\n                 source_path=None, context=None):\n        if metadata is None:\n            metadata = {}\n        if settings is None:\n            settings = copy.deepcopy(DEFAULT_CONFIG)\n\n        self.settings = settings\n        self._content = content\n        if context is None:\n            context = {}\n        self._context = context\n        self.translations = []\n\n        local_metadata = dict(settings['DEFAULT_METADATA'])\n        local_metadata.update(metadata)\n\n        # set metadata as attributes\n        for key, value in local_metadata.items():\n            if key in ('save_as', 'url'):\n                key = 'override_' + key\n            setattr(self, key.lower(), value)\n\n        # also keep track of the metadata attributes available\n        self.metadata = local_metadata\n\n        #default template if it's not defined in page\n        self.template = self._get_template()\n\n        # default author to the one in settings if not defined\n        if not hasattr(self, 'author'):\n            if 'AUTHOR' in settings:\n                self.author = Author(settings['AUTHOR'], settings)\n\n        # XXX Split all the following code into pieces, there is too much here.\n\n        # manage languages\n        self.in_default_lang = True\n        if 'DEFAULT_LANG' in settings:\n            default_lang = settings['DEFAULT_LANG'].lower()\n            if not hasattr(self, 'lang'):\n                self.lang = default_lang\n\n            self.in_default_lang = (self.lang == default_lang)\n\n        # create the slug if not existing, from the title\n        if not hasattr(self, 'slug') and hasattr(self, 'title'):\n            self.slug = slugify(self.title,\n                                settings.get('SLUG_SUBSTITUTIONS', ()))\n\n        self.source_path = source_path\n\n        # manage the date format\n        if not hasattr(self, 'date_format'):\n            if hasattr(self, 'lang') and self.lang in settings['DATE_FORMATS']:\n                self.date_format = settings['DATE_FORMATS'][self.lang]\n            else:\n                self.date_format = settings['DEFAULT_DATE_FORMAT']\n\n        if isinstance(self.date_format, tuple):\n            locale_string = self.date_format[0]\n            if sys.version_info < (3, ) and isinstance(locale_string,\n                                                       six.text_type):\n                locale_string = locale_string.encode('ascii')\n            locale.setlocale(locale.LC_ALL, locale_string)\n            self.date_format = self.date_format[1]\n\n        if hasattr(self, 'date'):\n            self.locale_date = strftime(self.date, self.date_format)\n\n        # manage status\n        if not hasattr(self, 'status'):\n            self.status = settings['DEFAULT_STATUS']\n            if not settings['WITH_FUTURE_DATES']:\n                if hasattr(self, 'date') and self.date > datetime.now():\n                    self.status = 'draft'\n\n        # store the summary metadata if it is set\n        if 'summary' in metadata:\n            self._summary = metadata['summary']\n\n        signals.content_object_init.send(self)\n\n    def __str__(self):\n        if self.source_path is None:\n            return repr(self)\n        elif six.PY3:\n            return self.source_path or repr(self)\n        else:\n            return str(self.source_path.encode('utf-8', 'replace'))\n\n    def check_properties(self):\n        \"\"\"Test mandatory properties are set.\"\"\"\n        for prop in self.mandatory_properties:\n            if not hasattr(self, prop):\n                raise NameError(prop)\n\n    @property\n    def url_format(self):\n        \"\"\"Returns the URL, formatted with the proper values\"\"\"\n        metadata = copy.copy(self.metadata)\n        path = self.metadata.get('path', self.get_relative_source_path())\n        default_category = self.settings['DEFAULT_CATEGORY']\n        slug_substitutions = self.settings.get('SLUG_SUBSTITUTIONS', ())\n        metadata.update({\n            'path': path_to_url(path),\n            'slug': getattr(self, 'slug', ''),\n            'lang': getattr(self, 'lang', 'en'),\n            'date': getattr(self, 'date', datetime.now()),\n            'author': slugify(\n                getattr(self, 'author', ''),\n                slug_substitutions\n            ),\n            'category': slugify(\n                getattr(self, 'category', default_category),\n                slug_substitutions\n            )\n        })\n        return metadata\n\n    def _expand_settings(self, key):\n        fq_key = ('%s_%s' % (self.__class__.__name__, key)).upper()\n        return self.settings[fq_key].format(**self.url_format)\n\n    def get_url_setting(self, key):\n        if hasattr(self, 'override_' + key):\n            return getattr(self, 'override_' + key)\n        key = key if self.in_default_lang else 'lang_%s' % key\n        return self._expand_settings(key)\n\n    def _update_content(self, content, siteurl):\n        \"\"\"Update the content attribute.\n\n        Change all the relative paths of the content to relative paths\n        suitable for the ouput content.\n\n        :param content: content resource that will be passed to the templates.\n        :param siteurl: siteurl which is locally generated by the writer in\n                        case of RELATIVE_URLS.\n        \"\"\"\n        if not content:\n            return content\n\n        hrefs = re.compile(r\"\"\"\n            (?P<markup><\\s*[^\\>]*  # match tag with src and href attr\n                (?:href|src)\\s*=)\n\n            (?P<quote>[\"\\'])      # require value to be quoted\n            (?P<path>\\|(?P<what>.*?)\\|(?P<value>.*?))  # the url value\n            \\2\"\"\", re.X)\n\n        def replacer(m):\n            what = m.group('what')\n            value = m.group('value')\n            origin = m.group('path')\n\n            # we support only filename for now. the plan is to support\n            # categories, tags, etc. in the future, but let's keep things\n            # simple for now.\n\n            # XXX Put this in a different location.\n            if what == 'filename':\n                if value.startswith('/'):\n                    value = value[1:]\n                else:\n                    # relative to the source path of this content\n                    value = self.get_relative_source_path(\n                        os.path.join(self.relative_dir, value)\n                    )\n\n                if value in self._context['filenames']:\n                    origin = '/'.join((siteurl,\n                             self._context['filenames'][value].url))\n                    origin = origin.replace('\\\\', '/')  # Fow windows paths.\n                else:\n                    logger.warning(\"Unable to find {fn}, skipping url\"\n                                   \" replacement\".format(fn=value))\n\n            return ''.join((m.group('markup'), m.group('quote'), origin,\n                            m.group('quote')))\n\n        return hrefs.sub(replacer, content)\n\n    @memoized\n    def get_content(self, siteurl):\n\n        if hasattr(self, '_get_content'):\n            content = self._get_content()\n        else:\n            content = self._content\n        return self._update_content(content, siteurl)\n\n    @property\n    def content(self):\n        return self.get_content(self._context.get('localsiteurl', ''))\n\n    def _get_summary(self):\n        \"\"\"Returns the summary of an article.\n\n        This is based on the summary metadata if set, otherwise truncate the\n        content.\n        \"\"\"\n        if hasattr(self, '_summary'):\n            return self._summary\n\n        if self.settings['SUMMARY_MAX_LENGTH'] is None:\n            return self.content\n\n        return truncate_html_words(self.content,\n                                   self.settings['SUMMARY_MAX_LENGTH'])\n\n    def _set_summary(self, summary):\n        \"\"\"Dummy function\"\"\"\n        pass\n\n    summary = property(_get_summary, _set_summary, \"Summary of the article.\"\n                       \"Based on the content. Can't be set\")\n    url = property(functools.partial(get_url_setting, key='url'))\n    save_as = property(functools.partial(get_url_setting, key='save_as'))\n\n    def _get_template(self):\n        if hasattr(self, 'template') and self.template is not None:\n            return self.template\n        else:\n            return self.default_template\n\n    def get_relative_source_path(self, source_path=None):\n        \"\"\"Return the relative path (from the content path) to the given\n        source_path.\n\n        If no source path is specified, use the source path of this\n        content object.\n        \"\"\"\n        if not source_path:\n            source_path = self.source_path\n        if source_path is None:\n            return None\n\n        return os.path.relpath(\n            os.path.abspath(os.path.join(self.settings['PATH'], source_path)),\n            os.path.abspath(self.settings['PATH'])\n        )\n\n    @property\n    def relative_dir(self):\n        return os.path.dirname(os.path.relpath(\n            os.path.abspath(self.source_path),\n            os.path.abspath(self.settings['PATH']))\n        )\n\n\nclass Page(Content):\n    mandatory_properties = ('title',)\n    default_template = 'page'\n\n\nclass Article(Page):\n    mandatory_properties = ('title', 'date', 'category')\n    default_template = 'article'\n\n\nclass Quote(Page):\n    base_properties = ('author', 'date')\n\n\n@python_2_unicode_compatible\nclass Static(Page):\n    @deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))\n    def filepath():\n        return None\n\n    @deprecated_attribute(old='src', new='source_path', since=(3, 2, 0))\n    def src():\n        return None\n\n    @deprecated_attribute(old='dst', new='save_as', since=(3, 2, 0))\n    def dst():\n        return None\n\n\ndef is_valid_content(content, f):\n    try:\n        content.check_properties()\n        return True\n    except NameError as e:\n        logger.error(\"Skipping %s: could not find information about \"\n                     \"'%s'\" % (f, e))\n        return False\n", "path": "pelican/contents.py"}]}