aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/post.py
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2015-08-26 07:57:23 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2015-08-26 07:57:23 -0300
commit70ceb871117ca811d63cb02671dc0fefc2700883 (patch)
tree846133ea39797d2cd1101cff2ac0818167353490 /nikola/post.py
parent8559119e2f45b7f6508282962c0430423bfab051 (diff)
parent787b97a4cb24330b36f11297c6d3a7a473a907d0 (diff)
Merge tag 'upstream/7.6.4'
Upstream version 7.6.4
Diffstat (limited to 'nikola/post.py')
-rw-r--r--nikola/post.py176
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