diff options
| author | 2015-08-26 07:57:23 -0300 | |
|---|---|---|
| committer | 2015-08-26 07:57:23 -0300 | |
| commit | 70ceb871117ca811d63cb02671dc0fefc2700883 (patch) | |
| tree | 846133ea39797d2cd1101cff2ac0818167353490 /nikola/post.py | |
| parent | 8559119e2f45b7f6508282962c0430423bfab051 (diff) | |
| parent | 787b97a4cb24330b36f11297c6d3a7a473a907d0 (diff) | |
Merge tag 'upstream/7.6.4'
Upstream version 7.6.4
Diffstat (limited to 'nikola/post.py')
| -rw-r--r-- | nikola/post.py | 176 |
1 files changed, 94 insertions, 82 deletions
diff --git a/nikola/post.py b/nikola/post.py index 466d5e0..7badfc6 100644 --- a/nikola/post.py +++ b/nikola/post.py @@ -24,6 +24,8 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""The Post class.""" + from __future__ import unicode_literals, print_function, absolute_import import io @@ -68,7 +70,7 @@ from .utils import ( ) from .rc4 import rc4 -__all__ = ['Post'] +__all__ = ('Post',) TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE) _UPGRADE_METADATA_ADVERTISED = False @@ -76,7 +78,7 @@ _UPGRADE_METADATA_ADVERTISED = False class Post(object): - """Represents a blog post or web page.""" + """Represent a blog post or site page.""" def __init__( self, @@ -102,7 +104,7 @@ class Post(object): if self.config['FUTURE_IS_NOW']: self.current_time = None else: - self.current_time = current_time() + self.current_time = current_time(tzinfo) self.translated_to = set([]) self._prev_post = None self._next_post = None @@ -231,6 +233,7 @@ class Post(object): self.compiler.register_extra_dependencies(self) def __repr__(self): + """Provide a representation of the post object.""" # Calculate a hash that represents most data about the post m = hashlib.md5() # source_path modification date (to avoid reading it) @@ -255,24 +258,32 @@ class Post(object): @property def alltags(self): - """This is ALL the tags for this post.""" + """Return ALL the tags for this post.""" tags = [] for l in self._tags: tags.extend(self._tags[l]) return list(set(tags)) - @property - def tags(self): - lang = nikola.utils.LocaleBorg().current_lang + def tags_for_language(self, lang): + """Return tags for a given language.""" if lang in self._tags: return self._tags[lang] + elif lang not in self.translated_to and self.skip_untranslated: + return [] elif self.default_lang in self._tags: return self._tags[self.default_lang] else: return [] @property + def tags(self): + """Return tags for the current language.""" + lang = nikola.utils.LocaleBorg().current_lang + return self.tags_for_language(lang) + + @property def prev_post(self): + """Return previous post.""" lang = nikola.utils.LocaleBorg().current_lang rv = self._prev_post while self.skip_untranslated: @@ -285,10 +296,12 @@ class Post(object): @prev_post.setter # NOQA def prev_post(self, v): + """Set previous post.""" self._prev_post = v @property def next_post(self): + """Return next post.""" lang = nikola.utils.LocaleBorg().current_lang rv = self._next_post while self.skip_untranslated: @@ -301,24 +314,32 @@ class Post(object): @next_post.setter # NOQA def next_post(self, v): + """Set next post.""" self._next_post = v @property def template_name(self): + """Return template name for this post.""" return self.meta('template') or self._template_name def formatted_date(self, date_format, date=None): - """Return the formatted date, as unicode.""" - if date: - fmt_date = date.strftime(date_format) + """Return the formatted date as unicode.""" + date = date if date else self.date + + if date_format == 'webiso': + # Formatted after RFC 3339 (web ISO 8501 profile) with Zulu + # zone desgignator for times in UTC and no microsecond precision. + fmt_date = date.replace(microsecond=0).isoformat().replace('+00:00', 'Z') else: - fmt_date = self.date.strftime(date_format) + fmt_date = date.strftime(date_format) + # Issue #383, this changes from py2 to py3 if isinstance(fmt_date, bytes_str): fmt_date = fmt_date.decode('utf8') return fmt_date def formatted_updated(self, date_format): + """Return the updated date as unicode.""" return self.formatted_date(date_format, self.updated) def title(self, lang=None): @@ -353,7 +374,7 @@ class Post(object): return self.meta[lang]['description'] def add_dependency(self, dependency, add='both', lang=None): - """Adds a file dependency for tasks using that post. + """Add a file dependency for tasks using that post. The ``dependency`` should be a string specifying a path, or a callable which returns such a string or a list of strings. @@ -365,7 +386,8 @@ class Post(object): includes the HTML resulting from compiling the fragment ('page' or 'both'). - If ``lang`` is not specified, this dependency is added for all languages.""" + If ``lang`` is not specified, this dependency is added for all languages. + """ if add not in {'fragment', 'page', 'both'}: raise Exception("Add parameter is '{0}', but must be either 'fragment', 'page', or 'both'.".format(add)) if add == 'fragment' or add == 'both': @@ -374,7 +396,7 @@ class Post(object): self._dependency_file_page[lang].append((type(dependency) != str, dependency)) def add_dependency_uptodate(self, dependency, is_callable=False, add='both', lang=None): - """Adds a dependency for task's ``uptodate`` for tasks using that post. + """Add a dependency for task's ``uptodate`` for tasks using that post. This can be for example an ``utils.config_changed`` object, or a list of such objects. @@ -397,7 +419,6 @@ class Post(object): post.add_dependency_uptodate( utils.config_changed({1: some_data}, 'uniqueid'), False, 'page') - """ if add == 'fragment' or add == 'both': self._dependency_uptodate_fragment[lang].append((is_callable, dependency)) @@ -433,13 +454,14 @@ class Post(object): deps.extend([cand_1, cand_2]) deps += self._get_dependencies(self._dependency_file_page[lang]) deps += self._get_dependencies(self._dependency_file_page[None]) - return deps + return sorted(deps) def deps_uptodate(self, lang): """Return a list of uptodate dependencies to build this post's page. These dependencies should be included in ``uptodate`` for the task - which generates the page.""" + which generates the page. + """ deps = [] deps += self._get_dependencies(self._dependency_uptodate_page[lang]) deps += self._get_dependencies(self._dependency_uptodate_page[None]) @@ -448,7 +470,6 @@ class Post(object): def compile(self, lang): """Generate the cache/ file with the compiled post.""" - def wrap_encrypt(path, password): """Wrap a post with encryption.""" with io.open(path, 'r+', encoding='utf8') as inf: @@ -480,7 +501,8 @@ class Post(object): """Return a list of uptodate dependencies to build this post's fragment. These dependencies should be included in ``uptodate`` for the task - which generates the fragment.""" + which generates the fragment. + """ deps = [] if self.default_lang in self.translated_to: deps.append(self.source_path) @@ -493,7 +515,7 @@ class Post(object): deps = [d for d in deps if os.path.exists(d)] deps += self._get_dependencies(self._dependency_file_fragment[lang]) deps += self._get_dependencies(self._dependency_file_fragment[None]) - return deps + return sorted(deps) def fragment_deps_uptodate(self, lang): """Return a list of file dependencies to build this post's fragment.""" @@ -504,7 +526,7 @@ class Post(object): return deps def is_translation_available(self, lang): - """Return true if the translation actually exists.""" + """Return True if the translation actually exists.""" return lang in self.translated_to def translated_source_path(self, lang): @@ -548,7 +570,6 @@ class Post(object): All links in the returned HTML will be relative. The HTML returned is a bare fragment, not a full document. """ - if lang is None: lang = nikola.utils.LocaleBorg().current_lang file_name = self._translated_file_path(lang) @@ -584,23 +605,23 @@ class Post(object): data = lxml.html.tostring(document, encoding='unicode') if teaser_only: - teaser = TEASER_REGEXP.split(data)[0] + teaser_regexp = self.config.get('TEASER_REGEXP', TEASER_REGEXP) + teaser = teaser_regexp.split(data)[0] if teaser != data: if not strip_html and show_read_more_link: - if TEASER_REGEXP.search(data).groups()[-1]: - teaser += '<p class="more"><a href="{0}">{1}</a></p>'.format( - self.permalink(lang), - TEASER_REGEXP.search(data).groups()[-1]) + if teaser_regexp.search(data).groups()[-1]: + teaser_text = teaser_regexp.search(data).groups()[-1] else: - l = self.config['RSS_READ_MORE_LINK'](lang) if rss_read_more_link else self.config['INDEX_READ_MORE_LINK'](lang) - teaser += l.format( - link=self.permalink(lang, query=rss_links_append_query), - read_more=self.messages[lang]["Read more"], - min_remaining_read=self.messages[lang]["%d min remaining to read"] % (self.remaining_reading_time), - reading_time=self.reading_time, - remaining_reading_time=self.remaining_reading_time, - paragraph_count=self.paragraph_count, - remaining_paragraph_count=self.remaining_paragraph_count) + teaser_text = self.messages[lang]["Read more"] + l = self.config['RSS_READ_MORE_LINK'](lang) if rss_read_more_link else self.config['INDEX_READ_MORE_LINK'](lang) + teaser += l.format( + link=self.permalink(lang, query=rss_links_append_query), + read_more=teaser_text, + min_remaining_read=self.messages[lang]["%d min remaining to read"] % (self.remaining_reading_time), + reading_time=self.reading_time, + remaining_reading_time=self.remaining_reading_time, + paragraph_count=self.paragraph_count, + remaining_paragraph_count=self.remaining_paragraph_count) # This closes all open tags and sanitizes the broken HTML document = lxml.html.fromstring(teaser) try: @@ -720,6 +741,7 @@ class Post(object): return path def permalink(self, lang=None, absolute=False, extension='.html', query=None): + """Return permalink for a post.""" if lang is None: lang = nikola.utils.LocaleBorg().current_lang @@ -746,6 +768,7 @@ class Post(object): @property def previewimage(self, lang=None): + """Return the previewimage path.""" if lang is None: lang = nikola.utils.LocaleBorg().current_lang @@ -759,13 +782,11 @@ class Post(object): return image_path def source_ext(self, prefix=False): - """ - Return the source file extension. + """Return the source file extension. If `prefix` is True, a `.src.` prefix will be added to the resulting extension - if it’s equal to the destination extension. + if it's equal to the destination extension. """ - ext = os.path.splitext(self.source_path)[1] # do not publish PHP sources if prefix and ext == '.html': @@ -778,7 +799,7 @@ class Post(object): def re_meta(line, match=None): - """re.compile for meta""" + """Find metadata using regular expressions.""" if match: reStr = re.compile('^\.\. {0}: (.*)'.format(re.escape(match))) else: @@ -793,10 +814,9 @@ def re_meta(line, match=None): def _get_metadata_from_filename_by_regex(filename, metadata_regexp, unslugify_titles): - """ - Tries to ried the metadata from the filename based on the given re. - This requires to use symbolic group names in the pattern. + """Try to reed the metadata from the filename based on the given re. + This requires to use symbolic group names in the pattern. The part to read the metadata from the filename based on a regular expression is taken from Pelican - pelican/readers.py """ @@ -816,7 +836,7 @@ def _get_metadata_from_filename_by_regex(filename, metadata_regexp, unslugify_ti def get_metadata_from_file(source_path, config=None, lang=None): - """Extracts metadata from the file itself, by parsing contents.""" + """Extract metadata from the file itself, by parsing contents.""" try: if lang and config: source_path = get_translation_candidate(config, source_path, lang) @@ -832,26 +852,10 @@ def get_metadata_from_file(source_path, config=None, lang=None): def _get_metadata_from_file(meta_data): - """Parse file contents and obtain metadata. - - >>> g = _get_metadata_from_file - >>> list(g([]).values()) - [] - >>> str(g(["======","FooBar","======"])["title"]) - 'FooBar' - >>> str(g(["FooBar","======"])["title"]) - 'FooBar' - >>> str(g(["#FooBar"])["title"]) - 'FooBar' - >>> str(g([".. title: FooBar"])["title"]) - 'FooBar' - >>> 'title' in g(["","",".. title: FooBar"]) - False - >>> 'title' in g(["",".. title: FooBar"]) # for #520 - True - - """ + """Extract metadata from a post's source file.""" meta = {} + if not meta_data: + return meta re_md_title = re.compile(r'^{0}([^{0}].*)'.format(re.escape('#'))) # Assuming rst titles are going to be at least 4 chars long @@ -859,37 +863,40 @@ def _get_metadata_from_file(meta_data): re_rst_title = re.compile(r'^([{0}]{{4,}})'.format(re.escape( string.punctuation))) + # Skip up to one empty line at the beginning (for txt2tags) + if not meta_data[0]: + meta_data = meta_data[1:] + + # First, get metadata from the beginning of the file, + # up to first empty line + for i, line in enumerate(meta_data): - # txt2tags requires an empty line at the beginning - # and since we are here because it's a 1-file post - # let's be flexible on what we accept, so, skip empty - # first lines. - if not line and i > 0: + if not line: break - if 'title' not in meta: - match = re_meta(line, 'title') - if match[0]: - meta['title'] = match[1] - if 'title' not in meta: + match = re_meta(line) + if match[0]: + meta[match[0]] = match[1] + + # If we have no title, try to get it from document + if 'title' not in meta: + piece = meta_data[:] + for i, line in enumerate(piece): if re_rst_title.findall(line) and i > 0: meta['title'] = meta_data[i - 1].strip() - if 'title' not in meta: + break if (re_rst_title.findall(line) and i >= 0 and re_rst_title.findall(meta_data[i + 2])): meta['title'] = meta_data[i + 1].strip() - if 'title' not in meta: + break if re_md_title.findall(line): meta['title'] = re_md_title.findall(line)[0] - - match = re_meta(line) - if match[0]: - meta[match[0]] = match[1] + break return meta def get_metadata_from_meta_file(path, config=None, lang=None): - """Takes a post path, and gets data from a matching .meta file.""" + """Take a post path, and gets data from a matching .meta file.""" global _UPGRADE_METADATA_ADVERTISED meta_path = os.path.splitext(path)[0] + '.meta' if lang and config: @@ -977,12 +984,15 @@ def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None) file_metadata_regexp, unslugify_titles)) + compiler_meta = {} + if getattr(post, 'compiler', None): compiler_meta = post.compiler.read_metadata(post, file_metadata_regexp, unslugify_titles, lang) meta.update(compiler_meta) - if not post.is_two_file: + if not post.is_two_file and not compiler_meta: # Meta file has precedence over file, which can contain garbage. + # Moreover, we should not to talk to the file if we have compiler meta. meta.update(get_metadata_from_file(post.source_path, config, lang)) if lang is None: @@ -1002,6 +1012,7 @@ def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None) def hyphenate(dom, _lang): + """Hyphenate a post.""" # circular import prevention from .nikola import LEGAL_VALUES lang = LEGAL_VALUES['PYPHEN_LOCALES'].get(_lang, pyphen.language_fallback(_lang)) @@ -1029,6 +1040,7 @@ def hyphenate(dom, _lang): def insert_hyphens(node, hyphenator): + """Insert hyphens into a node.""" textattrs = ('text', 'tail') if isinstance(node, lxml.etree._Entity): # HTML entities have no .text |
