├── acrylamid
├── lib
│ ├── CHANGES
│ ├── __init__.py
│ ├── requests.py
│ ├── _async.py
│ ├── httpd.py
│ ├── history.py
│ └── html.py
├── errors.py
├── filters
│ ├── hyph
│ │ ├── hyph-en-us.chr.txt
│ │ ├── hyph-fr.chr.txt
│ │ ├── hyph-de-1996.chr.txt
│ │ ├── hyph-de-1996.lic.txt
│ │ └── hyph-en-us.lic.txt
│ ├── rstx_sourcecode.py
│ ├── html.py
│ ├── pytextile.py
│ ├── replace.py
│ ├── python-discount.py
│ ├── head_offset.py
│ ├── rstx_highlight.py
│ ├── pandoc.py
│ ├── strip.py
│ ├── metalogo.py
│ ├── mako-templating.py
│ ├── mdx_subscript.py
│ ├── mdx_asciimathml.py
│ ├── rstx_gist.py
│ ├── rst.py
│ ├── rstx_youtube.py
│ ├── relative.py
│ ├── mdx_superscript.py
│ ├── md.py
│ ├── mdx_delins.py
│ ├── mdx_gist.py
│ ├── rstx_vimeo.py
│ ├── intro.py
│ └── jinja2-templating.py
├── assets
│ ├── fallback.py
│ └── web.py
├── views
│ ├── index.py
│ ├── articles.py
│ ├── search
│ │ ├── search.js
│ │ └── __init__.py
│ ├── category.py
│ ├── tag.py
│ ├── feeds.py
│ └── sitemap.py
├── compat.py
├── colors.py
├── templates
│ └── __init__.py
├── tasks
│ ├── deploy.py
│ ├── __init__.py
│ └── new.py
├── refs.py
├── defaults.py
├── log.py
└── hooks.py
├── docs
├── views
│ ├── feeds.rst
│ ├── other.rst
│ ├── index.rst
│ ├── single.rst
│ ├── multi.rst
│ └── search.rst
├── _themes
│ ├── werkzeug
│ │ ├── theme.conf
│ │ ├── layout.html
│ │ └── relations.html
│ ├── README
│ └── LICENSE
├── api
│ ├── helpers.rst
│ ├── core.rst
│ ├── readers.rst
│ └── lib.rst
├── extending.rst
├── assets.rst
├── filters
│ ├── markup
│ │ ├── other.rst
│ │ ├── rst.rst
│ │ └── md.rst
│ └── pre.rst
├── howtos.rst
├── conf.py
└── templating.rst
├── dist
└── acrylamid-0.8.dev0-py3.7.egg
├── .gitignore
├── .travis.yml
├── MANIFEST.in
├── AUTHORS
├── specs
├── views.py
├── mako.t
├── __init__.py
├── lib.py
├── core.py
├── utils.py
├── init.t
├── search.py
├── filters.py
├── entry.py
├── samples
│ └── vlent.nl.xml
├── translations.t
├── readers.py
└── helpers.py
├── requirements.txt
├── misc
├── search.py
└── bash_completion
├── LICENSE
└── setup.py
/acrylamid/lib/CHANGES:
--------------------------------------------------------------------------------
1 | ../../CHANGES
--------------------------------------------------------------------------------
/docs/views/feeds.rst:
--------------------------------------------------------------------------------
1 | Feeds
2 | =====
3 |
4 |
--------------------------------------------------------------------------------
/dist/acrylamid-0.8.dev0-py3.7.egg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/posativ/acrylamid/HEAD/dist/acrylamid-0.8.dev0-py3.7.egg
--------------------------------------------------------------------------------
/docs/_themes/werkzeug/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = werkzeug.css
4 | pygments_style = werkzeug_theme_support.WerkzeugStyle
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | *.pyc
3 | *.DS_Store
4 | /.tox/
5 | MANIFEST
6 | /docs/*build/
7 | /build/
8 | /acrylamid.egg-info/
9 | /src/
10 | venv/
11 | .vscode/
--------------------------------------------------------------------------------
/docs/api/helpers.rst:
--------------------------------------------------------------------------------
1 | ==============
2 | Helper Objects
3 | ==============
4 |
5 | .. module:: acrylamid.helpers
6 |
7 | .. automodule:: acrylamid.helpers
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/api/core.rst:
--------------------------------------------------------------------------------
1 | ==================
2 | Core Functionality
3 | ==================
4 |
5 | .. module:: acrylamid.core
6 |
7 | .. automodule:: acrylamid.core
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/api/readers.rst:
--------------------------------------------------------------------------------
1 | ==============
2 | Reader Objects
3 | ==============
4 |
5 | .. automethod:: acrylamid.readers.load
6 |
7 | .. autoclass:: acrylamid.readers.Entry
8 | :members:
9 | :inherited-members:
10 |
--------------------------------------------------------------------------------
/acrylamid/errors.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Martin Zimmermann Data!1 Data!1Related Topics
2 |
3 |
20 |
--------------------------------------------------------------------------------
/docs/views/other.rst:
--------------------------------------------------------------------------------
1 | Other
2 | =====
3 |
4 | .. _views-sitemap:
5 |
6 | Sitemap
7 | -------
8 |
9 | Create an XML-Sitemap where permalinks have the highest priority (1.0) and do
10 | never change and all other ressources have a changefreq of weekly.
11 |
12 | .. code-block:: python
13 |
14 | '/sitemap.xml': {
15 | 'view': 'Sitemap'
16 | }
17 |
18 | The sitemap by default excludes any resources copied over with the entry. If
19 | you wish to include image resources associated with the entry, the config
20 | property ``SITEMAP_IMAGE_EXT`` can be use to define file extensions to
21 | include. ``SITEMAP_RESOURCE_EXT`` can be used for other file types such as
22 | text files and PDFs. Video resources are not supported, and should not be
23 | included in the above properties.
24 |
--------------------------------------------------------------------------------
/acrylamid/filters/pytextile.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 sebix
4 | {%- for parent in parents %}
5 |
6 | {%- endfor %}
7 | {%- if prev %}
8 |
' % lang
32 | else:
33 | tmpl = '%%s
'
34 | html = tmpl % escape('\n'.join(self.content))
35 | raw = nodes.raw('', html, format='html')
36 | return [raw]
37 |
38 |
39 | def register(roles, directives):
40 | directives.register_directive('highlight-js', Highlight)
41 |
--------------------------------------------------------------------------------
/docs/extending.rst:
--------------------------------------------------------------------------------
1 | Extending Acrylamid
2 | ===================
3 |
4 | Acrylamid is designed to easily integrate custom code and you can customize
5 | almost everything: transformation to the content, custom HTML layout or a
6 | new view of your posts. Acrylamid itself is using this API to implement
7 | different text parsers like Markdown or reStructuredText, hyphenation and
8 | the complete rendering of articles, single posts and paged listings.
9 |
10 | Filters
11 | -------
12 |
13 | .. autoclass:: acrylamid.filters.Filter()
14 |
15 |
16 | Views
17 | -----
18 |
19 | .. autoclass:: acrylamid.views.View()
20 |
21 |
22 | Layout
23 | ------
24 |
25 | Acrylamid depends deeply on the popular Jinja2 template engine written in
26 | *pure* python. To work with Acrylamid each template you get from the
27 | environment object has a special attribute called ``has_changed`` and
28 | indicates over the whole compilation process if this template has changed
29 | or not. If a template inherits a template, we also check wether this has
30 | changed and so on.
31 |
32 | This allows us to write a simple statement wether we may skip a page or
33 | need to re-render it.
34 |
35 | .. todo:: make templates configurable
36 |
37 | - Jinja2 API docs
38 | - Jinja2 Designer Docs
39 |
--------------------------------------------------------------------------------
/acrylamid/lib/requests.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Martin Zimmermann %s
This is sugar: C6H12O6
' 17 | # 18 | # Paragraph breaks will nullify subscripts across paragraphs. Line breaks 19 | # within paragraphs will not. 20 | # 21 | # Modified to not subscript "~/Library. Foo bar, see ~/Music/". 22 | # 23 | # useful CSS rules: sup, sub { 24 | # vertical-align: baseline; 25 | # position: relative; 26 | # top: -0.4em; 27 | # } 28 | # sub { top: 0.4em; } 29 | 30 | import markdown 31 | 32 | match = ['subscript', 'sub'] 33 | 34 | 35 | class SubscriptPattern(markdown.inlinepatterns.Pattern): 36 | """Return a subscript Element: `C~6~H~12~O~6~'""" 37 | 38 | def handleMatch(self, m): 39 | 40 | text = m.group(3) 41 | 42 | if markdown.version_info < (2, 1, 0): 43 | el = markdown.etree.Element("sub") 44 | el.text = markdown.AtomicString(text) 45 | else: 46 | el = markdown.util.etree.Element("sub") 47 | el.text = markdown.util.AtomicString(text) 48 | 49 | return el 50 | 51 | 52 | class SubscriptExtension(markdown.Extension): 53 | """Subscript Extension for Python-Markdown.""" 54 | 55 | def extendMarkdown(self, md, md_globals): 56 | """Replace subscript with SubscriptPattern""" 57 | md.inlinePatterns['subscript'] = SubscriptPattern(r'(\~)([^\s\~]+)\2', md) 58 | 59 | 60 | def makeExtension(configs=None): 61 | return SubscriptExtension(configs=configs) 62 | -------------------------------------------------------------------------------- /acrylamid/filters/mdx_asciimathml.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2010-2011, Gabriele Favalessa 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, seeThis is a reference to a footnote1.
' 17 | # 18 | # >>> md.convert('This is scientific notation: 6.02 x 10^23^') 19 | # u'This is scientific notation: 6.02 x 1023
' 20 | # 21 | # >>> md.convert('This is scientific notation: 6.02 x 10^23. Note lack of second carat.') 22 | # u'This is scientific notation: 6.02 x 10^23. Note lack of second carat.
' 23 | # 24 | # >>> md.convert('Scientific notation: 6.02 x 10^23. Add carat at end of sentence.^') 25 | # u'Scientific notation: 6.02 x 1023. Add a carat at the end of sentence..
' 26 | # 27 | # Paragraph breaks will nullify superscripts across paragraphs. Line breaks 28 | # within paragraphs will not. 29 | # 30 | # Modified to not superscript "HEAD^1. Also for HEAD^2". 31 | # 32 | # useful CSS rules: sup, sub { 33 | # vertical-align: baseline; 34 | # position: relative; 35 | # top: -0.4em; 36 | # } 37 | # sub { top: 0.4em; } 38 | 39 | import markdown 40 | 41 | match = ['superscript', 'sup'] 42 | 43 | 44 | class SuperscriptPattern(markdown.inlinepatterns.Pattern): 45 | """Return a superscript Element (`word^2^`).""" 46 | 47 | def handleMatch(self, m): 48 | 49 | text = m.group(3) 50 | 51 | if markdown.version_info < (2, 1, 0): 52 | el = markdown.etree.Element("sup") 53 | el.text = markdown.AtomicString(text) 54 | else: 55 | el = markdown.util.etree.Element("sup") 56 | el.text = markdown.util.AtomicString(text) 57 | 58 | return el 59 | 60 | 61 | class SuperscriptExtension(markdown.Extension): 62 | """Superscript Extension for Python-Markdown.""" 63 | 64 | def extendMarkdown(self, md, md_globals): 65 | """Replace superscript with SuperscriptPattern""" 66 | md.inlinePatterns['superscript'] = SuperscriptPattern(r'(\^)([^\s\^]+)\2', md) 67 | 68 | 69 | def makeExtension(configs=None): 70 | return SuperscriptExtension(configs=configs) 71 | -------------------------------------------------------------------------------- /acrylamid/filters/md.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2012 Martin ZimmermannThis is added content and this is deleted content
47 | #
block (no embedded javascript)
28 | Add filters: [Markdown+gist] to your Markdown metadata"""
29 |
30 | def get_raw_gist_with_filename(self, gistID, filename):
31 | url = "https://raw.github.com/gist/%s/%s" % (gistID, filename)
32 | try:
33 | return get(url).read()
34 | except (URLError, HTTPError) as e:
35 | log.exception('Failed to access URL %s : %s' % (url, e))
36 | return ''
37 |
38 | def get_raw_gist(self, gistID):
39 | url = "https://raw.github.com/gist/%s" % (gistID)
40 | try:
41 | return get(url).read()
42 | except (URLError, HTTPError) as e:
43 | log.exception('Failed to access URL %s : %s' % (url, e))
44 | return ''
45 |
46 | def handleMatch(self, m):
47 |
48 | if markdown.version_info < (2, 1, 0):
49 | mdutils = markdown
50 | else:
51 | mdutils = markdown.util
52 |
53 | gistID = m.group('gistID')
54 | gistFilename = m.group('filename')
55 |
56 | if gistFilename:
57 | embeddedJS = "https://gist.github.com/%s.js?file=%s" % (gistID, gistFilename)
58 | rawGist = (self.get_raw_gist_with_filename(gistID, gistFilename))
59 | else:
60 | embeddedJS = "https://gist.github.com/%s.js" % (gistID)
61 | rawGist = (self.get_raw_gist(gistID))
62 |
63 | if self.pattern == GIST_RE:
64 | el = mdutils.etree.Element('div')
65 | el.set('class', 'gist')
66 | script = mdutils.etree.SubElement(el, 'script')
67 | script.set('src', embeddedJS)
68 |
69 | # NoScript alternative in block
70 | noscript = mdutils.etree.SubElement(el, 'noscript')
71 | pre = mdutils.etree.SubElement(noscript, 'pre')
72 | pre.set('class', 'literal-block')
73 | pre.text = mdutils.AtomicString(rawGist)
74 | else:
75 | # No javascript, just output gist as wrapped text
76 | el = mdutils.etree.Element('pre')
77 | el.set('class', 'literal-block gist-raw')
78 | el.text = mdutils.AtomicString(rawGist)
79 |
80 | return el
81 |
82 |
83 | def makeExtension(configs=None):
84 | return GistExtension(configs=configs)
85 |
--------------------------------------------------------------------------------
/acrylamid/filters/rstx_vimeo.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 the_metalgamer . All rights reserved.
4 | # License: BSD Style, 2 clauses. see acrylamid/__init__.py
5 |
6 | from docutils import nodes
7 | from docutils.parsers.rst import Directive, directives
8 |
9 | import re
10 |
11 | color_pattern = re.compile("([a-f]|[A-F]|[0-9]){3}(([a-f]|[A-F]|[0-9]){3})")
12 |
13 | match = ['vimeo']
14 |
15 |
16 | def align(argument):
17 | return directives.choice(argument, ('left', 'center', 'right'))
18 |
19 | def color(argument):
20 | match = color_pattern.match(argument)
21 | if match:
22 | return argument
23 | else:
24 | raise ValueError('argument must be an hexadecimal color number')
25 |
26 |
27 | class Vimeo(Directive):
28 | """Vimeo directive for easy embedding (`:options:` are optional).
29 |
30 | .. code-block:: rst
31 |
32 | .. vimeo:: 6455561
33 | :align: center
34 | :height: 1280
35 | :width: 720
36 | :border: 1px
37 | :color: ffffff
38 | :nobyline:
39 | :noportrait:
40 | :nobyline:
41 | :notitle:
42 | :autoplay:
43 | :loop:
44 | """
45 |
46 | required_arguments = 1
47 | optional_arguments = 0
48 | option_spec = {
49 | 'height': directives.length_or_unitless,
50 | 'width': directives.length_or_unitless,
51 | 'align': align,
52 | 'border': directives.length_or_unitless,
53 | 'color': color,
54 | 'noportrait': directives.flag,
55 | 'notitle': directives.flag,
56 | 'nobyline': directives.flag,
57 | 'autoplay': directives.flag,
58 | 'loop': directives.flag,
59 | }
60 | has_content = False
61 |
62 | def run(self):
63 |
64 | alignments = {
65 | 'left': '0',
66 | 'center': '0 auto',
67 | 'right': '0 0 0 auto',
68 | }
69 |
70 | self.options.setdefault('color', 'ffffff')
71 |
72 | uri = ("http://player.vimeo.com/video/" + self.arguments[0]
73 | + ( "?color=" + self.options['color'] + "&" ) \
74 | + ( "title=0&" if 'notitle' in self.options else "") \
75 | + ( "portrait=0&" if 'noportrait' in self.options else "") \
76 | + ( "byline=0&" if 'nobyline' in self.options else "") \
77 | + ( "autoplay=1&" if 'autoplay' in self.options else "") \
78 | + ( "loop=1" if 'loop' in self.options else "" )
79 | )
80 | self.options['uri'] = uri
81 | self.options['align'] = alignments[self.options.get('align', 'center')]
82 | self.options.setdefault('width', '500px')
83 | self.options.setdefault('height', '281px')
84 | self.options.setdefault('border', '0')
85 |
86 | VI_EMBED = """"""
89 | return [nodes.raw('', VI_EMBED % self.options, format='html')]
90 |
91 | def register(roles, directives):
92 | directives.register_directive('vimeo', Vimeo)
93 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import sys
4 | import os
5 | import datetime
6 | import pkg_resources
7 |
8 | from distutils.version import LooseVersion
9 |
10 | # If extensions (or modules to document with autodoc) are in another directory,
11 | # add these directories to sys.path here. If the directory is relative to the
12 | # documentation root, use os.path.abspath to make it absolute, like shown here.
13 | sys.path.append(os.path.abspath('_themes'))
14 |
15 | # -- General configuration -----------------------------------------------------
16 |
17 | extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.blockdiag', 'sphinx.ext.mathjax']
18 | templates_path = ['_templates']
19 | source_suffix = '.rst'
20 | master_doc = 'index'
21 |
22 | # ADJUST IT TO FIT YOUR NEEDS!1
23 | # blockdiag_fontpath = '/usr/share/fonts/truetype/ipafont/ipagp.ttf'
24 | blockdiag_fontpath = '/Users/ich/Library/Fonts/DejaVuSans-Bold.ttf'
25 |
26 | # General information about the project.
27 | project = u'Acrylamid'
28 | copyright = u'%i, Martin Zimmermann' % datetime.date.today().year
29 |
30 | release = pkg_resources.get_distribution("acrylamid").version # 0.6, 0.6.1, 0.7 or 0.7.1
31 | version = '%i.%i' % tuple(LooseVersion(release).version[:2]) # 0.6 or 0.7
32 |
33 | # The language for content autogenerated by Sphinx. Refer to documentation
34 | # for a list of supported languages.
35 | #language = None
36 |
37 | exclude_patterns = ['_build']
38 |
39 | html_theme = 'werkzeug'
40 | html_theme_path = ['_themes']
41 | html_static_path = ['_static']
42 |
43 | htmlhelp_basename = 'acrylamiddoc'
44 |
45 | # -- Options for LaTeX output --------------------------------------------------
46 |
47 | latex_documents = [
48 | ('index', 'acrylamid.tex', u'Acrylamid Documentation',
49 | u'Martin Zimmermann', 'manual'),
50 | ]
51 |
52 | # The name of an image file (relative to this directory) to place at the top of
53 | # the title page.
54 | #latex_logo = None
55 |
56 | # For "manual" documents, if this is true, then toplevel headings are parts,
57 | # not chapters.
58 | #latex_use_parts = False
59 |
60 | # If true, show page references after internal links.
61 | #latex_show_pagerefs = False
62 |
63 | # If true, show URL addresses after external links.
64 | #latex_show_urls = False
65 |
66 | # Additional stuff for the LaTeX preamble.
67 | #latex_preamble = ''
68 |
69 | # Documents to append as an appendix to all manuals.
70 | #latex_appendices = []
71 |
72 | # If false, no module index is generated.
73 | #latex_domain_indices = True
74 |
75 |
76 | # -- Options for manual page output --------------------------------------------
77 |
78 | # One entry per manual page. List of tuples
79 | # (source start file, name, description, authors, manual section).
80 | man_pages = [
81 | ('index', 'acrylamid', u'Acrylamid Documentation',
82 | [u'Martin Zimmermann'], 1)
83 | ]
84 |
85 | # -- http://stackoverflow.com/questions/7825263/including-docstring-in-sphinx-documentation --
86 | from sphinx.ext import autodoc
87 |
88 | class SimpleDocumenter(autodoc.ClassDocumenter):
89 | objtype = "simple"
90 |
91 | #do not indent the content
92 | content_indent = ""
93 |
94 | #do not add a header to the docstring
95 | def add_directive_header(self, sig):
96 | pass
97 |
98 | def setup(app):
99 | app.add_autodocumenter(SimpleDocumenter)
100 |
--------------------------------------------------------------------------------
/docs/views/multi.rst:
--------------------------------------------------------------------------------
1 | Multi-Post
2 | ==========
3 |
4 | .. _views-index:
5 |
6 | Index
7 | -----
8 |
9 | .. _views-archive:
10 |
11 | Archive
12 | -------
13 |
14 | .. _views-category:
15 |
16 | Category
17 | --------
18 |
19 | A view to recursively render all posts of a category, sub category and
20 | so on. Configuration syntax:
21 |
22 | .. code-block:: python
23 |
24 | '/category/:name/': {
25 | 'view': 'category',
26 | 'pagination': '/category/:name/:num/'
27 | }
28 |
29 | Categories are either explicitly set in the metadata section
30 | (`category: foo/bar`) or derived from the path of the post, e.g.
31 | `content/projects/python/foo-bar.txt` will set the category to `projects/python`.
32 |
33 | .. code-block:: sh
34 |
35 | $ tree content/
36 | content/
37 | ├── projects
38 | │ ├── bla.txt
39 | │ └── python
40 | │ └── fuu.txt
41 | └── test
42 | └── sample-entry.txt
43 |
44 | The directory structure above then renders the following:
45 |
46 | .. code-block:: sh
47 |
48 | $ acrylamid compile
49 | create [0.00s] output/category/test/index.html
50 | create [0.00s] output/category/projects/index.html
51 | create [0.00s] output/category/projects/python/index.html
52 |
53 | Both, bla.txt and fuu.txt are shown on the project listing, but only the
54 | fuu.txt post appears on the projects/python listing.
55 |
56 | Categories in the Entry Context
57 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 |
59 | To link to the category listing, use the following Jinja2 code instead of
60 | the tag code in the `entry.html` template:
61 |
62 | .. code-block:: html+jinja
63 |
64 | {% if 'category' in env.views and entry.category %}
65 | categorized in
66 | {% for link in entry.category | categorize %}
67 | {{ link.title }}
68 | {%- if loop.revindex > 2 -%}
69 | ,
70 | {%- elif loop.revindex == 2 %}
71 | and
72 | {% endif %}
73 | {% endfor %}
74 |
75 |
76 | This uses the new `categorize` filter which is available when you have the
77 | category view enabled. It takes the category list from `entry.category` and
78 | yields the hierarchical categories, e.g. projects/python yields projects and
79 | projects/python.
80 |
81 | Categories in the Environment Context
82 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83 |
84 | Similar to the tag cloud you are able to generate a category listing on
85 | each page: ``env.categories`` is an iterable object that yields categories
86 | (which are iterable as well and yield sub categories). The following Jinja2
87 | code recursively renders all categories and sub categories in a simple list:
88 |
89 | .. code-block:: html+jinja
90 |
91 |
92 | {% for category in env.categories recursive %}
93 |
94 | - Category: {{ category.title }}
95 | with {{ category.items | count }} articles.
96 |
97 |
98 | {% if category %}
99 | {{ loop(category) }}
100 | {% endif %}
101 |
102 | {% endfor %}
103 |
104 |
105 |
106 | .. versionadded:: 0.8
107 |
108 | Support for categories was introduced.
109 |
110 | .. _views-tag:
111 |
112 | Tag
113 | ---
114 |
--------------------------------------------------------------------------------
/acrylamid/views/search/search.js:
--------------------------------------------------------------------------------
1 | /* Copyright 2013, Martin Zimmermann . All rights reserved.
2 | * License: BSD Style, 2 clauses. See LICENSE for details.
3 | */
4 |
5 | /*
6 | * Searches the (lowercase) term in suffix tree. Returns either a valid query
7 | * or undefined for no match.
8 | */
9 | var search = function(term) {
10 |
11 | var keyword = term.toLowerCase(), haystack, prefix;
12 |
13 | if (keyword.length < 2)
14 | return;
15 |
16 | if (!(keyword[0] in search.tree)) {
17 | prefix = keyword.charCodeAt(0) < 123 ? keyword[0] : "_";
18 |
19 | search.req.abort();
20 | search.req.open("GET", search.path + prefix + ".js", false);
21 | search.req.send()
22 |
23 | if (search.req.status != 200)
24 | return;
25 |
26 | haystack = JSON.parse(search.req.response);
27 | if (prefix == "_") {
28 | for (var attr in haystack) {
29 | search.tree[attr] = haystack[attr];
30 | }
31 | } else {
32 | search.tree[prefix] = haystack;
33 | }
34 | }
35 |
36 | return search.query(keyword.substring(1), search.tree[keyword[0]]);
37 | }
38 |
39 | /*
40 | * Search `needle` in `haystack` in something around O(m). Returns a tuple
41 | * with first, the exact matches, and last the partial matches.
42 | */
43 | search.query = function (needle, haystack) {
44 |
45 | // find partial matches
46 | function find(node) {
47 |
48 | var rv = [];
49 |
50 | if (typeof node == "undefined")
51 | return rv;
52 |
53 | if (node.length == 2) {
54 | for (var item in node[1])
55 | rv.push(node[1][item]);
56 | }
57 |
58 | for (var key in node[0]) {
59 | rv += find(node[0][key]);
60 | }
61 |
62 | return rv;
63 | }
64 |
65 | var node = haystack, partials = [],
66 | i = 0, j = 0;
67 |
68 | while (j < needle.length) {
69 | if (node[0][needle.substring(i,j+1)]) {
70 | node = node[0][needle.substring(i,j+1)]
71 | i = j + 1;
72 | }
73 |
74 | j++;
75 | }
76 |
77 | if (i != j) // no suffix found
78 | return;
79 |
80 | for (var key in node[0])
81 | partials = partials.concat(find(node[0][key]));
82 |
83 | return [node[1], partials];
84 | }
85 |
86 | /*
87 | * Return context around `keyword` from `id`, you can use `limit` to receive
88 | * up to N paragraphs containing `keyword`. Returns an array or undefined.
89 | */
90 | search.context = function(keyword, id, limit) {
91 |
92 | var req = new XMLHttpRequest(),
93 | rv = new Array(), source;
94 |
95 | if (typeof limit == "undefined") {
96 | limit = 1;
97 | }
98 |
99 | req.open("GET", search.path + "src/" + id + ".txt", false);
100 | req.send();
101 |
102 | if (req.status != 200)
103 | return;
104 |
105 | source = req.response.split(/\n\n/);
106 | for (var chunk in source) {
107 | if (source[chunk].toLowerCase().indexOf(keyword.toLowerCase()) > -1) {
108 | if (limit == 0)
109 | break;
110 |
111 | rv.push(source[chunk]);
112 | limit--;
113 | }
114 | }
115 |
116 | return rv;
117 | }
118 |
119 | search.req = new XMLHttpRequest();
120 | search.tree = {}
121 |
122 | search.path = %% PATH %%;
123 | search.lookup = %% ENTRYLIST %%;
124 |
--------------------------------------------------------------------------------
/acrylamid/lib/html.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | """
7 | Generic HTML tools
8 | ~~~~~~~~~~~~~~~~~~
9 |
10 | A collection of tools that ease reading and writing HTML. Currently,
11 | there's only a improved version of python's :class:`HTMLParser.HTMLParser`,
12 | that returns the HTML untouched, so you can override specific calls to
13 | add custom behavior.
14 |
15 | This implementation is used :mod:`acrylamid.filters.acronyms`,
16 | :mod:`acrylamid.filters.hyphenation` and more advanced in
17 | :mod:`acrylamid.filters.summarize`. It is quite fast, but remains
18 | an unintuitive way of working with HTML."""
19 |
20 | import sys
21 | import re
22 |
23 | from cgi import escape
24 | from acrylamid.compat import PY2K, unichr
25 |
26 | from html.parser import HTMLParser as DefaultParser
27 | from html.entities import name2codepoint
28 |
29 |
30 | def unescape(s):
31 | """& -> & conversion"""
32 | return re.sub('&(%s);' % '|'.join(name2codepoint),
33 | lambda m: unichr(name2codepoint[m.group(1)]), s)
34 |
35 |
36 | def format(attrs):
37 | res = []
38 | for key, value in attrs:
39 | if value is None:
40 | res.append(key)
41 | else:
42 | res.append('%s="%s"' % (key, escape(value, quote=True)))
43 | return ' '.join(res)
44 |
45 |
46 | if sys.version_info < (3, 0):
47 | class WTFMixin(object, DefaultParser):
48 | pass
49 | else:
50 | class WTFMixin(DefaultParser):
51 | pass
52 |
53 |
54 | class HTMLParser(WTFMixin):
55 | """A more useful base HTMLParser that returns the actual HTML by
56 | default::
57 |
58 | >>> "Foo" == HTMLParser("Foo").result
59 |
60 | It is intended to use this class as base so you don't make
61 | the same mistakes I did before.
62 |
63 | .. attribute:: result
64 |
65 | This is the processed HTML."""
66 |
67 | def __init__(self, html):
68 | DefaultParser.__init__(self)
69 | self.result = []
70 | self.stack = []
71 |
72 | self.feed(html)
73 |
74 | def handle_starttag(self, tag, attrs):
75 | """Append tag to stack and write it to result."""
76 |
77 | self.stack.append(tag)
78 | self.result.append('<%s %s>' % (tag, format(attrs)) if attrs else '<%s>' % tag)
79 |
80 | def handle_data(self, data):
81 | """Everything that is *not* a tag shows up as data, but you can't expect
82 | that it is always a continous sentence or word."""
83 |
84 | self.result.append(data)
85 |
86 | def handle_endtag(self, tag):
87 | """Append ending tag to result and pop it from the stack too."""
88 |
89 | try:
90 | self.stack.pop()
91 | except IndexError:
92 | pass
93 | self.result.append('%s>' % tag)
94 |
95 | def handle_startendtag(self, tag, attrs):
96 | """Something like ``"
"``"""
97 | self.result.append('<%s %s/>' % (tag, format(attrs)))
98 |
99 | def handle_entityref(self, name):
100 | """An escaped ampersand like ``"&"``."""
101 | self.result.append('&' + name + ';')
102 |
103 | def handle_charref(self, char):
104 | """An escaped umlaut like ``"ä"``"""
105 | self.result.append('' + char + ';')
106 |
107 | def handle_comment(self, comment):
108 | """Preserve HTML comments."""
109 | self.result.append('')
110 |
111 | __all__ = ['HTMLParser', 'unescape']
112 |
--------------------------------------------------------------------------------
/acrylamid/tasks/new.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2013 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | import sys
7 | import io
8 | import os
9 | import tempfile
10 | import subprocess
11 | import shutil
12 | import shlex
13 |
14 | from os.path import join, dirname, isfile, splitext
15 | from datetime import datetime
16 |
17 | from acrylamid import log, readers, commands
18 | from acrylamid.errors import AcrylamidException
19 | from acrylamid.compat import string_types
20 |
21 | from acrylamid.tasks import task, argument
22 | from acrylamid.utils import force_unicode as u
23 | from acrylamid.helpers import safe, event
24 |
25 | try:
26 | input = raw_input
27 | except NameError:
28 | pass
29 |
30 | yaml, rst, md = \
31 | lambda title, date: u"---\ntitle: %s\ndate: %s\n---\n\n" % (safe(title), date), \
32 | lambda title, date: u"%s\n" % title + "="*len(title) + '\n\n' + ":date: %s\n\n" % date, \
33 | lambda title, date: u"Title: %s\nDate: %s\n\n" % (title, date)
34 |
35 | formats = {'.md': md, '.mkdown': md, '.rst': rst, '.rest': rst}
36 |
37 |
38 | @task('new', [argument("title", nargs="*", default=None)], help="create a new entry")
39 | def run(conf, env, options):
40 | """Subcommand: new -- create a new blog entry the easy way. Either run
41 | ``acrylamid new My fresh new Entry`` or interactively via ``acrylamid new``
42 | and the file will be created using the preferred permalink format."""
43 |
44 | # we need the actual default values
45 | commands.initialize(conf, env)
46 |
47 | # config content_extension originally defined as string, not a list
48 | extlist = conf.get('content_extension',['.txt'])
49 | if isinstance(extlist, string_types):
50 | ext = extlist
51 | else:
52 | ext = extlist[0]
53 |
54 | fd, tmp = tempfile.mkstemp(suffix=ext, dir='.cache/')
55 |
56 | editor = os.getenv('VISUAL') if os.getenv('VISUAL') else os.getenv('EDITOR')
57 | tt = formats.get(ext, yaml)
58 |
59 | if options.title:
60 | title = u(' '.join(options.title))
61 | else:
62 | title = u(input("Entry's title: "))
63 |
64 | with io.open(fd, 'w', encoding='utf-8') as f:
65 | f.write(tt(title, datetime.now().strftime(conf['date_format'])))
66 |
67 | entry = readers.Entry(tmp, conf)
68 | p = join(conf['content_dir'], splitext(entry.permalink.strip('/'))[0])
69 |
70 | try:
71 | os.makedirs(p.rsplit('/', 1)[0])
72 | except OSError:
73 | pass
74 |
75 | filepath = p + ext
76 | if isfile(filepath):
77 | raise AcrylamidException('Entry already exists %r' % filepath)
78 | shutil.move(tmp, filepath)
79 | event.create('new', filepath)
80 |
81 | if datetime.now().hour == 23 and datetime.now().minute > 45:
82 | log.info("notice don't forget to update entry.date-day after mignight!")
83 |
84 | if log.level() >= log.WARN:
85 | return
86 |
87 | try:
88 | if editor:
89 | retcode = subprocess.call(shlex.split(editor) + [filepath])
90 | elif sys.platform == 'darwin':
91 | retcode = subprocess.call(['open', filepath])
92 | else:
93 | retcode = subprocess.call(['xdg-open', filepath])
94 | except OSError:
95 | raise AcrylamidException('Could not launch an editor')
96 |
97 | # XXX process detaches... m(
98 | if retcode < 0:
99 | raise AcrylamidException('Child was terminated by signal %i' % -retcode)
100 |
101 | if os.stat(filepath)[6] == 0:
102 | raise AcrylamidException('File is empty!')
103 |
--------------------------------------------------------------------------------
/specs/entry.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import tempfile
4 | import attest
5 |
6 | from datetime import datetime
7 |
8 | from acrylamid import log
9 | from acrylamid.errors import AcrylamidException
10 | from acrylamid.compat import iteritems
11 |
12 | from acrylamid.readers import Entry
13 | from acrylamid.defaults import conf
14 |
15 | log.init('acrylamid', level=40)
16 | conf['entry_permalink'] = '/:year/:slug/'
17 |
18 | def create(path, **kwargs):
19 |
20 | with open(path, 'w') as fp:
21 | fp.write('---\n')
22 | for k, v in iteritems(kwargs):
23 | fp.write('%s: %s\n' % (k, v))
24 | fp.write('---\n')
25 |
26 |
27 | class TestEntry(attest.TestBase):
28 |
29 | def __context__(self):
30 | fd, self.path = tempfile.mkstemp(suffix='.txt')
31 | yield
32 |
33 | @attest.test
34 | def dates(self):
35 |
36 | create(self.path, date='13.02.2011, 15:36', title='bla')
37 | date = Entry(self.path, conf).date.replace(tzinfo=None)
38 |
39 | assert date.year == 2011
40 | assert date.month == 2
41 | assert date.day == 13
42 | assert date == datetime(year=2011, month=2, day=13, hour=15, minute=36)
43 |
44 | @attest.test
45 | def alternate_dates(self):
46 |
47 | create(self.path, date='1.2.2034', title='bla')
48 | date = Entry(self.path, conf).date.replace(tzinfo=None)
49 |
50 | assert date.year == 2034
51 | assert date.month == 2
52 | assert date.day == 1
53 | assert date == datetime(year=2034, month=2, day=1)
54 |
55 | @attest.test
56 | def invalid_dates(self):
57 |
58 | create(self.path, date='unparsable', title='bla')
59 | with attest.raises(AcrylamidException):
60 | Entry(self.path, conf).date
61 |
62 | @attest.test
63 | def permalink(self):
64 |
65 | create(self.path, title='foo')
66 | entry = Entry(self.path, conf)
67 |
68 | assert entry.permalink == '/2013/foo/'
69 |
70 | create(self.path, title='foo', permalink='/hello/world/')
71 | entry = Entry(self.path, conf)
72 |
73 | assert entry.permalink == '/hello/world/'
74 |
75 | create(self.path, title='foo', permalink_format='/:year/:slug/index.html')
76 | entry = Entry(self.path, conf)
77 |
78 | assert entry.permalink == '/2013/foo/'
79 |
80 | @attest.test
81 | def tags(self):
82 |
83 | create(self.path, title='foo', tags='Foo')
84 | assert Entry(self.path, conf).tags == ['Foo']
85 |
86 | create(self.path, title='foo', tags='[Foo, Bar]')
87 | assert Entry(self.path, conf).tags == ['Foo', 'Bar']
88 |
89 | @attest.test
90 | def deprecated_keys(self):
91 |
92 | create(self.path, title='foo', tag=[], filter=[])
93 | entry = Entry(self.path, conf)
94 |
95 | assert 'tags' in entry
96 | assert 'filters' in entry
97 |
98 | @attest.test
99 | def custom_values(self):
100 |
101 | create(self.path, title='foo', image='/img/test.png')
102 | entry = Entry(self.path, conf)
103 |
104 | assert 'image' in entry
105 | assert entry.image == '/img/test.png'
106 |
107 | @attest.test
108 | def fallbacks(self):
109 |
110 | create(self.path, title='Bla')
111 | entry = Entry(self.path, conf)
112 |
113 | assert entry.draft == False
114 | assert entry.email == 'info@example.com'
115 | assert entry.author == 'Anonymous'
116 | assert entry.extension == 'txt'
117 | assert entry.year == datetime.now().year
118 | assert entry.imonth == datetime.now().month
119 | assert entry.iday == datetime.now().day
120 |
--------------------------------------------------------------------------------
/specs/samples/vlent.nl.xml:
--------------------------------------------------------------------------------
1 |
2 | tag:www.vlent.nl,2010-02-04:/weblog/atom.xml Mark van Lent's weblog Practicing software development http://www.vlent.nl/static/images/favicon.ico http://www.vlent.nl/static/images/favicon.ico 2012-08-09T14:05:28Z Mark van Lent Creative Commons Attribution 3.0 Unported License tag:www.vlent.nl,2012-08-09:/weblog/2012/08/09/attributeerror-querymethodid-when-creating-object/ "AttributeError: queryMethodId" when creating an object 2012-08-09T14:05:28Z Mark van Lent <div><strong><p>While working on a client project, I created an (Archetypes based) content type with a text field. After adding a custom view as the default view, I got an <code>AttributeError</code> when I tried to add a new object. </p></strong></div><div><p>Some details about the content type:</p>
3 | <ul>
4 | <li>It includes a <code>TextField</code> which uses the <code>RichWidget</code> (read: TinyMCE).</li>
5 | <li>I changed the <code>default_view</code> setting from <code>folder_listing</code> to <code>view</code> in the Generic Setup configuration file (<code>types/WikiPage.xml</code> in my case).</li>
6 | </ul>
7 | <p>Whenever I tried to add a new object, I got the following traceback:</p>
8 | <pre><code> ...
9 | Module zope.tal.talinterpreter, line 583, in do_setLocal_tal
10 | Module zope.tales.tales, line 696, in evaluate
11 | - URL: file:home/mark/eggs/Products.TinyMCE-1.2.11-py2.6.egg/Products/TinyMCE/skins/tinymce/tinymce_wysiwyg_support.pt
12 | - Line 6, Column 2
13 | - Expression: <PathExpr standard:u'object|here'>
14 | - Names:
15 | {'container': <PloneSite at /site>,
16 | ...
17 | 'user': <PropertiedUser 'admin'>}
18 | Module zope.tales.expressions, line 217, in __call__
19 | Module Products.PageTemplates.Expressions, line 155, in _eval
20 | Module Products.PageTemplates.Expressions, line 117, in render
21 | Module Products.CMFDynamicViewFTI.browserdefault, line 76, in __call__
22 | Module Products.CMFPlone.PloneFolder, line 122, in __call__
23 | AttributeError: queryMethodId
24 | </code></pre>
25 | <p>If I removed the text field or set the default view back to <code>folder_listing</code>, the error did not present itself.</p>
26 | <p>To make a long story short: in the end it appears to be an issue with Products.TinyMCE version 1.2.11. And since that version is included in Plone 4.1.5, I spent quite some time figuring out why my new content type didn't work while a similar content type in an older project did. Figuring I had done something wrong, I did not immediately realise that the older project was using Plone 4.1.4 (and thus an older version of Products.TinyMCE that does <em>not</em> have this issue)…</p>
27 | <p><strong>The solution:</strong> pin Products.TinyMCE to version 1.2.12. Or you could just use Plone 4.1.6 or 4.2, which both include the fixed version by default.</p></div>
--------------------------------------------------------------------------------
/acrylamid/filters/intro.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Mark van Lent . All rights reserved.
4 | # License: BSD Style, 2 clauses.
5 |
6 | from acrylamid import log, helpers
7 | from acrylamid.filters import Filter
8 | from acrylamid.lib.html import HTMLParser
9 |
10 |
11 | class Introducer(HTMLParser):
12 | paragraph_list = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'ul', 'ol', 'pre', 'p']
13 | """List of root elements, which may be treated as paragraphs"""
14 |
15 | def __init__(self, html, maxparagraphs, href, options):
16 | self.maxparagraphs = maxparagraphs
17 | self.paragraphs = 0
18 | self.options = options
19 | self.href = href
20 |
21 | super(Introducer, self).__init__(html)
22 |
23 | def handle_starttag(self, tag, attrs):
24 | if self.paragraphs < self.maxparagraphs:
25 | super(Introducer, self).handle_starttag(tag, attrs)
26 |
27 | def handle_data(self, data):
28 | if self.paragraphs >= self.maxparagraphs:
29 | pass
30 | elif len(self.stack) < 1 or (self.stack[0] not in self.paragraph_list and self.stack[-1] not in self.paragraph_list):
31 | pass
32 | else:
33 | self.result.append(data)
34 |
35 | def handle_endtag(self, tag):
36 | if self.paragraphs < self.maxparagraphs:
37 | if tag in self.paragraph_list:
38 | self.paragraphs += 1
39 | super(Introducer, self).handle_endtag(tag)
40 |
41 | if self.paragraphs == self.maxparagraphs:
42 | for x in self.stack[:]:
43 | self.result.append('%s>' % self.stack.pop())
44 | if self.options['link'] != '':
45 | self.result.append(self.options['link'] % self.href)
46 |
47 | def handle_startendtag(self, tag, attrs):
48 | if self.paragraphs < self.maxparagraphs and tag not in self.options['ignore']:
49 | super(Introducer, self).handle_startendtag(tag, attrs)
50 |
51 | def handle_entityref(self, name):
52 | if self.paragraphs < self.maxparagraphs:
53 | super(Introducer, self).handle_entityref(name)
54 |
55 | def handle_charref(self, char):
56 | if self.paragraphs < self.maxparagraphs:
57 | super(Introducer, self).handle_charref(char)
58 |
59 | def handle_comment(self, comment):
60 | if self.paragraphs < self.maxparagraphs:
61 | super(Introducer, self).handle_comment(comment)
62 |
63 |
64 | class Introduction(Filter):
65 |
66 | match = ['intro', ]
67 | version = 2
68 | priority = 15.0
69 |
70 | defaults = {
71 | 'ignore': ['img', 'video', 'audio'],
72 | 'link': '…continue.'
73 | }
74 |
75 | @property
76 | def uses(self):
77 | return self.env.path
78 |
79 | def transform(self, content, entry, *args):
80 | options = helpers.union(Introduction.defaults, self.conf.fetch('intro_'))
81 |
82 | try:
83 | options.update(entry.intro)
84 | except AttributeError:
85 | pass
86 |
87 | try:
88 | maxparagraphs = int(options.get('maxparagraphs') or args[0])
89 | except (IndexError, ValueError) as ex:
90 | if isinstance(ex, ValueError):
91 | log.warn('Introduction: invalid maxparagraphs argument %r',
92 | options.get('maxparagraphs') or args[0])
93 | maxparagraphs = 1
94 |
95 | try:
96 | return ''.join(Introducer(
97 | content, maxparagraphs, self.env.path+entry.permalink, options).result)
98 | except:
99 | log.exception('could not extract intro from ' + entry.filename)
100 | return content
101 | return content
102 |
--------------------------------------------------------------------------------
/acrylamid/views/category.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2013 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | from itertools import chain
7 |
8 | from acrylamid.views.index import Index, Paginator
9 | from acrylamid.compat import itervalues, iteritems
10 | from acrylamid.helpers import expand, safeslug
11 |
12 |
13 | def fetch(tree):
14 | """fetch all posts from the tree"""
15 |
16 | for item in tree[1]:
17 | yield item
18 |
19 | for subtree in itervalues(tree[0]):
20 | for item in fetch(subtree):
21 | yield item
22 |
23 |
24 | def recurse(category, tree):
25 |
26 | yield category, sorted(list(fetch(tree)), key=lambda k: k.date, reverse=True)
27 |
28 | for subtree in iteritems(tree[0]):
29 | for item in recurse(category + '/' + safeslug(subtree[0]), subtree[1]):
30 | yield item
31 |
32 |
33 | class Top(object):
34 | """Top-level category node without a category at all. Iterable and yields
35 | sub categories that are also iterable up to the very last sub category."""
36 |
37 | def __init__(self, tree, route):
38 | self.tree = tree
39 | self.route = route
40 | self.parent = []
41 |
42 | def __iter__(self):
43 | for category, subtree in sorted(iteritems(self.tree[0]), key=lambda k: k[0]):
44 | yield Subcategory(self.parent + [category], category, subtree, self.route)
45 |
46 | def __bool__(self):
47 | return len(self) > 0
48 |
49 | @property
50 | def items(self):
51 | return list(fetch(self.tree))
52 |
53 | @property
54 | def href(self):
55 | return expand(self.route, {'name': ''})
56 |
57 |
58 | class Subcategory(Top):
59 |
60 | def __init__(self, parent, category, tree, route):
61 | self.parent = parent
62 | self.title = category
63 | self.tree = tree
64 | self.route = route
65 |
66 | def __str__(self):
67 | return self.title
68 |
69 | @property
70 | def href(self):
71 | return expand(self.route, {'name': '/'.join(map(safeslug, self.parent))})
72 |
73 |
74 | class Category(Index):
75 |
76 | export = ['prev', 'curr', 'next', 'items_per_page', 'category', 'entrylist']
77 | template = 'main.html'
78 |
79 | def context(self, conf, env, data):
80 |
81 | self.tree = ({}, [])
82 |
83 | for entry in data['entrylist']:
84 | node = self.tree
85 |
86 | for i, category in enumerate(entry.category):
87 |
88 | if i < len(entry.category) - 1:
89 | if category in node:
90 | node = node[category]
91 | else:
92 | node = node[0].setdefault(category, ({}, []))
93 | else:
94 | node[0].setdefault(category, ({}, []))[1].append(entry)
95 |
96 | class Link:
97 |
98 | def __init__(self, title, href):
99 | self.title = title
100 | self.href = href if href.endswith('/') else href + '/'
101 |
102 | def categorize(category):
103 | for i, name in enumerate(category):
104 | rv = '/'.join(category[:i] + [name])
105 | yield Link(rv, expand(self.path, {'name': rv}))
106 |
107 | env.engine.register('categorize', categorize)
108 | env.categories = Top(self.tree, self.path)
109 | return env
110 |
111 | def generate(self, conf,env, data):
112 |
113 | iterator = chain(*map(lambda args: recurse(*args), iteritems(self.tree[0])))
114 |
115 | for category, entrylist in iterator:
116 | data['entrylist'] = entrylist
117 | for res in Paginator.generate(self, conf, env, data,
118 | category=category, name=category):
119 | yield res
120 |
--------------------------------------------------------------------------------
/acrylamid/views/search/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2013 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | import re
7 | import io
8 | import json
9 | import string
10 |
11 | from os.path import join, dirname
12 | from collections import defaultdict
13 |
14 | from acrylamid.views import View
15 | from acrylamid.compat import iteritems
16 | from acrylamid.helpers import joinurl
17 |
18 |
19 | def commonprefix(a, b):
20 | """Find longest common prefix of `a` and `b`."""
21 |
22 | pos = 0
23 | length = min(len(a), len(b))
24 |
25 | while pos < length and a[pos] == b[pos]:
26 | pos += 1
27 |
28 | return pos, b
29 |
30 |
31 | def insert(tree, word, refs):
32 |
33 | # get top-level node
34 | node, prev = tree.setdefault(word[0], ({}, )), None
35 |
36 | i = 0
37 | while i < len(word) - 1:
38 |
39 | try:
40 | index, prefix = max(commonprefix(word[i+1:], key) for key in node[0]
41 | if word[i+1] == key[0])
42 | except ValueError:
43 | index, prefix = 0, None
44 |
45 | if prefix and index == len(prefix) and index != len(word[i+1:]):
46 | prev, node = node, node[0][prefix]
47 | i += index
48 |
49 | # has common sub prefix, retain compression
50 | elif 0 < index < len(prefix):
51 |
52 | rv = node[0].pop(prefix)
53 | a, b = prefix[:index], prefix[index:]
54 |
55 | i += len(a)
56 |
57 | node[0][a] = ({b: rv}, )
58 | node, prev = node[0][a], node
59 |
60 | if i == len(word) - 1:
61 | prev[0][a] = (node[0], refs)
62 | break
63 |
64 | # not yet saved, append
65 | else:
66 | node[0][word[i+1:]] = (node[0].get(word[i+1:], ({}, ))[0], refs)
67 | break
68 |
69 |
70 | def index(entrylist):
71 | """Build compressed suffix tree in something around O(n * log(n)), but with
72 | huge time constants. It is *really* slow but more space efficient, hopefully."""
73 |
74 | tree, meta = {}, []
75 | words = defaultdict(set)
76 |
77 | for num, entry in enumerate(entrylist):
78 | meta.append((entry.permalink, entry.title))
79 |
80 | for word in re.split(r"[.:,\s!?=\(\)]+", entry.content):
81 | if len(word) < 3:
82 | continue
83 | for i in range(len(word) - 3):
84 | words[word[i:].lower()].add(num)
85 |
86 | for key, value in iteritems(words):
87 | insert(tree, key, list(value))
88 |
89 | del words
90 | return tree, meta
91 |
92 |
93 | class Search(View):
94 |
95 | def generate(self, conf, env, request):
96 |
97 | if not env.options.search:
98 | raise StopIteration()
99 |
100 | tree, meta = index(request['entrylist'])
101 |
102 | for i, entry in enumerate(request['entrylist']):
103 | yield io.StringIO(entry.content), \
104 | joinurl(conf['output_dir'], self.path, 'src', '%i.txt' % i)
105 |
106 | # CST algorithm with `meta` data
107 | with io.open(join(dirname(__file__), 'search.js'), encoding='utf-8') as fp:
108 | javascript = fp.read()
109 |
110 | fp = io.StringIO((javascript
111 | .replace('%% PATH %%', json.dumps(self.path))
112 | .replace('%% ENTRYLIST %%', json.dumps(meta))))
113 | yield fp, joinurl(conf['output_dir'], self.path, 'search.js')
114 |
115 | for char in string.ascii_lowercase:
116 | if char in tree:
117 | fp = io.BytesIO()
118 | json.dump(tree.pop(char), fp)
119 |
120 | yield fp, joinurl(conf['output_dir'], self.path, char + '.js')
121 |
122 | fp = io.BytesIO()
123 | json.dump(tree, fp)
124 | yield fp, joinurl(conf['output_dir'], self.path, '_.js')
125 |
--------------------------------------------------------------------------------
/docs/templating.rst:
--------------------------------------------------------------------------------
1 | Templating
2 | ==========
3 |
4 | The default theme is very minimalistic and may don't fit to everyone's
5 | expectations. But like every blog compiler, you can edit the complete layout to
6 | your likings.
7 |
8 | Unlike others Acrylamid knows when you edited a template file, hence when you do
9 | a session on templates and just the your changes without any interaction, launch
10 | Acrylamid in auto-compile mode (``acrylamid autocompile``) and even when you
11 | have hundrets of postings you will see your result almost immediately appear.
12 |
13 | Variables
14 | ---------
15 |
16 | Internally all configuration variables are written in small caps, therefore
17 | this listing differs from :doc:`conf.py`.
18 |
19 | conf
20 | ****
21 |
22 | Global configuration, :doc:`conf.py`.
23 |
24 | env
25 | ***
26 |
27 | Environment which contains some useful informations like version info, current
28 | protocol and count of entries.
29 |
30 | :protocol:
31 | Internet protocol determined from ``sitename``, should be http or https.
32 |
33 | :netloc:
34 | Domain name like *domain.example.tld*, derieved from ``sitename``
35 |
36 | :path:
37 | Path (sub-uri) from ``sitename``, used for relative links.
38 |
39 | :views:
40 | A list of executed views, only used to explicitely activate certain features like Disqus-integration or tagging in templates.
41 |
42 | :type:
43 | Current view type to distuingish between single entry, tag or page view. Can be one of this list: ['entry', 'tag', 'index'].
44 |
45 | :entrylist:
46 | A list of all posts to be rendered in a template.
47 |
48 | :globals.entrylist:
49 | All entries.
50 |
51 | :globals.pages:
52 | All pages.
53 |
54 | :globals.translations:
55 | All translations, if the translation view is active.
56 |
57 | :globals.drafts:
58 | All drafts, if the draft view is active.
59 |
60 | :num_entries:
61 | Count of all entries, only available in page/articles/index-view.
62 |
63 | :prev:
64 | Number of the previous pagination index if available, e.g. ``'3'`` or ``None``.
65 |
66 | :curr:
67 | Number of the current pagination index, e.g. ``2``.
68 |
69 | :next:
70 | Number of the next pagination index if available, e.g. ``1`` or ``None``
71 |
72 | :route:
73 | The (expanded) path of the view, e.g. ``articles``, ``/2012/my-example`` (for ``/:year/:slug/``) or
74 | ``/tag/acrylamid/`` (for ``/tag/:name``).
75 |
76 | :version:
77 | Acrylamid's version.
78 |
79 | entry
80 | *****
81 |
82 | :type:
83 | either ``"entry"`` or ``"page"``
84 |
85 | :permalink:
86 | actual permanent link
87 |
88 | :date:
89 | entry's :class:`datetime.datetime` object
90 |
91 | :year:
92 | entry's year (Integer)
93 |
94 | :month:
95 | zero padded month number of the entry, e.g. "05" for May and "11"
96 | for November (String)
97 |
98 | :imonth:
99 | entry's month (Integer)
100 |
101 | :day:
102 | zero padded day number of the entry, e.g. "04", "17" (String)
103 |
104 | :iday:
105 | entry's day (Integer)
106 |
107 | :filters:
108 | a per-post applied filters as list
109 |
110 | :tags:
111 | per-post list of applied tags
112 |
113 | :title:
114 | entry's title
115 |
116 | :author:
117 | entry's author as set in entry or from conf.py if unset
118 |
119 | :lang:
120 | entry's language, derieved from conf.py if unset
121 |
122 | :content:
123 | returns rendered content
124 |
125 | :description:
126 | first 50 characters from the source
127 |
128 | :slug:
129 | safe entry title
130 |
131 | :draft:
132 | if set to True, the entry will not appear in articles, index, feed and tag view
133 |
134 | :path:
135 | filename's path without content_dir and extension, e.g. "content/2013/foo.md"
136 | gets "2013/foo".
137 |
138 | :extension:
139 | filename's extension without leading dot
140 |
141 | :resources:
142 | a list of resource file paths copied with the entry via the ``copy`` metadata attribute
--------------------------------------------------------------------------------
/specs/translations.t:
--------------------------------------------------------------------------------
1 | Test translation feature
2 |
3 | $ [ -n "$PYTHON" ] || PYTHON="`which python`"
4 | $ LANG="en_US.UTF-8" && unset LC_ALL && unset LANGUAGE
5 | $ acrylamid init -q $TMPDIR
6 | $ cd $TMPDIR
7 |
8 | Add a german and english post. No translation route so far.
9 |
10 | $ cat > content/hello-de.txt << EOF
11 | > ---
12 | > title: Hallo Welt
13 | > lang: de
14 | > date: 12.12.2012, 13:24
15 | > identifier: hello
16 | > ---
17 | > Hallo Welt!
18 | > EOF
19 |
20 | $ cat > content/hello.txt << EOF
21 | > ---
22 | > title: Hello World
23 | > date: 12.12.2012, 13:23
24 | > identifier: hello
25 | > ---
26 | > Hello World!
27 | > EOF
28 |
29 | $ acrylamid compile -Cv
30 | create [0.??s] output/articles/index.html (glob)
31 | create [?.??s] output/2012/hallo-welt/index.html (glob)
32 | create [0.??s] output/2012/hello-world/index.html (glob)
33 | create [0.??s] output/2012/die-verwandlung/index.html (glob)
34 | create [0.??s] output/index.html (glob)
35 | create [0.??s] output/tag/die-verwandlung/index.html (glob)
36 | create [0.??s] output/tag/franz-kafka/index.html (glob)
37 | create [0.??s] output/atom/index.html (glob)
38 | create [0.??s] output/rss/index.html (glob)
39 | create [0.??s] output/sitemap.xml (glob)
40 | create output/style.css
41 | 11 new, 0 updated, 0 skipped [?.??s] (glob)
42 |
43 | Now we add the translation view to /:year/:slug/:lang/, this should affect a
44 | re-generation of all views since an entry has vanished.
45 |
46 | $ if [ `uname` = "Linux" ]; then
47 | > sed -i -e /translation/,/translation/s/[#].// conf.py
48 | > else
49 | > sed -i "" -e /translation/,/translation/s/[#].// conf.py
50 | > fi
51 |
52 | $ acrylamid compile -Cv
53 | update [0.??s] output/articles/index.html (glob)
54 | update [0.??s] output/2012/hello-world/index.html (glob)
55 | identical output/2012/die-verwandlung/index.html
56 | create [0.??s] output/2012/hallo-welt/de/index.html (glob)
57 | update [0.??s] output/index.html (glob)
58 | identical output/tag/die-verwandlung/index.html
59 | identical output/tag/franz-kafka/index.html
60 | update [0.??s] output/atom/index.html (glob)
61 | update [0.??s] output/rss/index.html (glob)
62 | update [0.??s] output/sitemap.xml (glob)
63 | skip output/style.css
64 | 1 new, 6 updated, 4 skipped [?.??s] (glob)
65 |
66 | When we now edit the translation, it should not affect anything.
67 |
68 | $ echo "Ohai." >> content/hello-de.txt
69 | $ acrylamid co -Cv
70 | skip output/articles/index.html
71 | skip output/2012/hello-world/index.html
72 | skip output/2012/die-verwandlung/index.html
73 | update [?.??s] output/2012/hallo-welt/de/index.html (glob)
74 | skip output/index.html
75 | skip output/tag/die-verwandlung/index.html
76 | skip output/tag/franz-kafka/index.html
77 | skip output/atom/index.html
78 | skip output/rss/index.html
79 | identical output/sitemap.xml
80 | skip output/style.css
81 | 0 new, 1 updated, 10 skipped [?.??s] (glob)
82 |
83 | But if we edit the title, the references should check for any updates.
84 |
85 | $ if [ `uname` = "Linux" ]; then
86 | > sed -i "s/title: Hallo Welt/title: Ohai Welt/" content/hello-de.txt
87 | > else
88 | > sed -i "" "s/title: Hallo Welt/title: Ohai Welt/" content/hello-de.txt
89 | > fi
90 | $ acrylamid co -Cv
91 | identical output/articles/index.html
92 | identical output/2012/hello-world/index.html
93 | identical output/2012/die-verwandlung/index.html
94 | create [?.??s] output/2012/ohai-welt/de/index.html (glob)
95 | identical output/index.html
96 | identical output/tag/die-verwandlung/index.html
97 | identical output/tag/franz-kafka/index.html
98 | identical output/atom/index.html
99 | identical output/rss/index.html
100 | update [0.??s] output/sitemap.xml (glob)
101 | skip output/style.css
102 | 1 new, 1 updated, 9 skipped [?.??s] (glob)
103 |
104 | Clean up.
105 |
106 | $ rm -rf output/ theme/ content/ .cache/ conf.py
107 |
--------------------------------------------------------------------------------
/acrylamid/filters/jinja2-templating.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | import io
7 | import re
8 | import types
9 |
10 | from os.path import join, isfile
11 |
12 | from acrylamid import log
13 | from acrylamid.errors import AcrylamidException
14 | from acrylamid.compat import PY2K, text_type as str
15 |
16 | from acrylamid.filters import Filter
17 | from acrylamid.helpers import system as defaultsystem
18 |
19 | from jinja2 import Environment, TemplateError
20 |
21 |
22 | class Jinja2(Filter):
23 | """Jinja2 filter that pre-processes in Markdown/reStructuredText
24 | written posts. XXX: and offers some jinja2 extensions."""
25 |
26 | match = ['Jinja2', 'jinja2']
27 | version = 1
28 |
29 | priority = 90.0
30 |
31 | def init(self, conf, env, *args):
32 |
33 | def system(cmd, stdin=None):
34 | try:
35 | return defaultsystem(cmd, stdin, shell=True).strip()
36 | except (OSError, AcrylamidException) as e:
37 | log.warn('%s: %s' % (e.__class__.__name__, e.args[0]))
38 | return e.args[0]
39 |
40 | self.conf = conf
41 | self.env = env
42 |
43 | # jinja2 is limited and can't import any module
44 | import time, datetime, os.path
45 | modules = [time, datetime, os.path]
46 |
47 | # check config for imports
48 | confimports = conf.get('jinja2_import')
49 | if confimports and isinstance(confimports, list):
50 | for modname in confimports:
51 | try:
52 | modules.append(__import__(modname))
53 | except ImportError as e:
54 | log.exception('Failed loading user defined Jinja2 import: '
55 | '%s (JINJA2_IMPORT = %s)' % (e, confimports))
56 |
57 | if PY2K:
58 | import urllib
59 | modules += [urllib]
60 | else:
61 | import urllib.request, urllib.parse, urllib.error
62 | modules += [urllib.request, urllib.parse, urllib.error]
63 |
64 | if isinstance(env.engine._jinja2, Environment):
65 | self.jinja2_env = env.engine._jinja2.overlay(cache_size=0)
66 | else:
67 | self.jinja2_env = Environment(cache_size=0)
68 |
69 | self.jinja2_env.filters['system'] = system
70 | self.jinja2_env.filters['split'] = str.split
71 |
72 | # swap out platform specific os.path name (posixpath , ntpath, riscospath)
73 | ospathmodname, os.path.__name__ = os.path.__name__, 'os.path'
74 |
75 | for mod in modules:
76 | for name in dir(mod):
77 | if name.startswith('_') or isinstance(getattr(mod, name), types.ModuleType):
78 | continue
79 |
80 | self.jinja2_env.filters[mod.__name__ + '.' + name] = getattr(mod, name)
81 |
82 | # restore original os.path module name
83 | os.path.__name__ = ospathmodname
84 |
85 | @property
86 | def macros(self):
87 | """Import macros from ``THEME/macro.html`` into context of the
88 | post environment. Very hackish, but it should work."""
89 |
90 | path = join(self.conf['theme'][0], 'macros.html')
91 | if not (isfile(path) and isinstance(self.env.engine._jinja2, Environment)):
92 | return ''
93 |
94 | with io.open(path, encoding='utf-8') as fp:
95 | text = fp.read()
96 |
97 | return "{%% from 'macros.html' import %s with context %%}\n" % ', '.join(
98 | re.findall('^\{% macro ([^\(]+)', text, re.MULTILINE))
99 |
100 | def transform(self, content, entry):
101 |
102 | try:
103 | tt = self.jinja2_env.from_string(self.macros + content)
104 | return tt.render(conf=self.conf, env=self.env, entry=entry)
105 | except (TemplateError, AcrylamidException) as e:
106 | log.warn('%s: %s in %r' % (e.__class__.__name__, e.args[0], entry.filename))
107 | return content
108 |
--------------------------------------------------------------------------------
/acrylamid/log.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | from __future__ import print_function
7 |
8 | import sys
9 | import logging
10 | import warnings
11 | from logging import INFO, WARN, DEBUG
12 | from acrylamid.colors import bold, red, green, yellow, black
13 |
14 | SKIP = 15
15 | logger = fatal = critical = warn = info = skip = debug = error = exception = None
16 |
17 |
18 | class TerminalHandler(logging.StreamHandler):
19 | """A handler that logs everything >= logging.WARN to stderr and everything
20 | below to stdout."""
21 |
22 | def __init__(self):
23 | logging.StreamHandler.__init__(self)
24 | self.stream = None # reset it; we are not going to use it anyway
25 |
26 | def emit(self, record):
27 | if record.levelno >= logging.WARN:
28 | self.__emit(record, sys.stderr)
29 | else:
30 | self.__emit(record, sys.stdout)
31 |
32 | def __emit(self, record, strm):
33 | self.stream = strm
34 | logging.StreamHandler.emit(self, record)
35 |
36 |
37 | class ANSIFormatter(logging.Formatter):
38 | """Implements basic colored output using ANSI escape codes. Currently acrylamid
39 | uses nanoc's color and information scheme: skip, create, identical, update,
40 | re-initialized, removed.
41 |
42 | If log level is greater than logging.WARN the level name is printed red underlined.
43 | """
44 |
45 | def __init__(self, fmt='[%(levelname)s] %(name)s: %(message)s'):
46 | logging.Formatter.__init__(self, fmt)
47 |
48 | def format(self, record):
49 |
50 | keywords = {'create': green, 'update': yellow, 'skip': black, 'identical': black,
51 | 're-initialized': yellow, 'remove': black, 'notice': black, 'execute': black}
52 |
53 | if record.levelno in (SKIP, INFO):
54 | for item in keywords:
55 | if record.msg.startswith(item):
56 | record.msg = record.msg.replace(item, ' '*2 + \
57 | keywords[item](bold(item.rjust(9))))
58 | elif record.levelno >= logging.WARN:
59 | record.levelname = record.levelname.replace('WARNING', 'WARN')
60 | record.msg = ''.join([' '*2, u"" + red(bold(record.levelname.lower().rjust(9))),
61 | ' ', record.msg])
62 |
63 | return logging.Formatter.format(self, record)
64 |
65 |
66 | class SkipHandler(logging.Logger):
67 | """Adds ``skip`` as new log item, which has a value of 15
68 |
69 | via """
70 | def __init__(self, name, level=logging.NOTSET):
71 | logging.Logger.__init__(self, name, level)
72 |
73 | def skip(self, msg, *args, **kwargs):
74 | self.log(15, msg, *args, **kwargs)
75 |
76 |
77 | def init(name, level, colors=True):
78 |
79 | global logger, critical, fatal, warn, info, skip, debug, error, exception
80 |
81 | logging.setLoggerClass(SkipHandler)
82 | logger = logging.getLogger(name)
83 |
84 | handler = TerminalHandler()
85 | if colors:
86 | handler.setFormatter(ANSIFormatter('%(message)s'))
87 |
88 | logger.addHandler(handler)
89 | logger.setLevel(level)
90 |
91 | error = logger.error
92 | fatal = logger.fatal
93 | critical = logger.critical
94 | warn = logger.warn
95 | info = logger.info
96 | skip = logger.skip
97 | debug = logger.debug
98 | exception = logger.exception
99 |
100 | warnings.resetwarnings()
101 | warnings.showwarning = showwarning if level == DEBUG else lambda *x: None
102 |
103 |
104 | def setLevel(level):
105 | global logger
106 | logger.setLevel(level)
107 |
108 |
109 | def level():
110 | global logger
111 | return logger.level
112 |
113 |
114 | def showwarning(msg, cat, path, lineno):
115 | print(path + ':%i' % lineno)
116 | print('%s: %s' % (cat().__class__.__name__, msg))
117 |
118 |
119 | __all__ = ['fatal', 'warn', 'info', 'skip', 'debug', 'error',
120 | 'WARN', 'INFO', 'SKIP', 'DEBUG', 'setLevel', 'level']
121 |
--------------------------------------------------------------------------------
/docs/filters/pre.rst:
--------------------------------------------------------------------------------
1 | Preprocessors
2 | =============
3 |
4 | Filters that are executed before compiling the markup language to HTML are
5 | called preprocessors.
6 |
7 | .. _filters-pre-jinja2:
8 |
9 | Jinja2
10 | ------
11 |
12 | In addition to HTML+jinja2 templating you can also use `Jinja2
13 | `_ in your postings, which may be useful when
14 | implementing a image gallery or other repeative tasks.
15 |
16 | Within jinja you have a custom ``system``-filter which allows you to call
17 | something like ``ls`` directly in your content (use it with care, when you
18 | rebuilt this content, the output might differ).
19 |
20 | ::
21 |
22 | ---
23 | title: "Jinja2's system filter"
24 | filters: jinja2
25 | ---
26 |
27 | Take a look at my code:
28 |
29 | .. code-block:: python
30 |
31 | {{ "cat ~/work/project/code.py" | system | indent(4) }}
32 |
33 | You can find my previous article "{{ env.prev.title }}" here_. Not
34 | interesting enough? How about lorem ipsum?
35 |
36 | {{ lipsum(5) }}
37 |
38 | .. _here: {{ env.prev }}
39 |
40 | Environment variables are the same as in :doc:`templating` plus some imported
41 | modules from Python namely: ``time``, ``datetime`` and ``urllib`` because you
42 | can't import anything from Jinja2. You can also access the root templating
43 | environment when Jinja2. This means, you can import and inherit from templates
44 | located in your theme folder.
45 |
46 | For convenience, the Jinja2 filter automatically imports every macro from
47 | ``macros.html`` into your post context, so there is no need for a
48 | ``{% from 'macros.html' import foo %}``.
49 |
50 | ============ ==================================================
51 | Requires
52 | Aliases Jinja2, jinja2
53 | ============ ==================================================
54 |
55 | .. _filters-pre-mako:
56 |
57 | Mako
58 | ----
59 |
60 | Just like Jinja2 filtering but using Mako. You have also ``system`` filter
61 | available within Mako. Unlike Jinja2 Mako can import python modules during
62 | runtime, therefore no additional modules are imported into the namespace.
63 |
64 | ============ ==================================================
65 | Requires `mako `_
66 | Aliases Mako, mako
67 | ============ ==================================================
68 |
69 |
70 | .. _filters-pre-liquid:
71 |
72 | Liquid
73 | ------
74 |
75 | Implementation of most plugins of the Jekyll/Octopress project. This filter
76 | (unfortunately) can not be used with reST or any other markup language, that
77 | can not handle inline HTML.
78 |
79 | The liquid filters are useful of you are migrating from Jekyll/Octopress or
80 | look for an inofficial standard (rather than custom Markdown extensions) that
81 | is used by Jekyll_/Octopress_, Hexo_.
82 |
83 | .. _Jekyll: https://github.com/mojombo/jekyll/wiki/Liquid-Extensions#tags
84 | .. _Octopress: http://octopress.org/docs/plugins/
85 | .. _Hexo: http://zespia.tw/hexo/docs/tag-plugins.html
86 |
87 | Currently, the following tags are ported (I reference the Octopress plugin
88 | documentation for usage details):
89 |
90 | - blockquote__ -- generate beautiful, semantic block quotes
91 | - img__ -- easily post images with class names and titles
92 | - youtube__ -- easy embedding of YouTube videos
93 | - pullquote__ -- generate CSS only pull quotes — no duplicate data, no javascript
94 | - tweet__ -- embed tweets using Twitter's oEmbed API
95 |
96 | __ http://octopress.org/docs/plugins/blockquote/
97 | __ http://octopress.org/docs/plugins/image-tag/
98 | __ http://www.portwaypoint.co.uk/jekyll-youtube-liquid-template-tag-gist/
99 | __ http://octopress.org/docs/plugins/pullquote/
100 | __ https://github.com/scottwb/jekyll-tweet-tag
101 |
102 | If you need another plugin, just ask on `GitHub:Issues
103 | `_ (plugins that will not
104 | implemented in near future: Include Array, Render Partial, Code Block).
105 |
106 | ============ ==================================================
107 | Requires
108 | Aliases liquid, octopress
109 | ============ ==================================================
110 |
--------------------------------------------------------------------------------
/docs/views/search.rst:
--------------------------------------------------------------------------------
1 | Search
2 | ======
3 |
4 | A space-efficient and fast full text search engine for Acrylamid using
5 | compressed `suffix trees`_ (CST). In comparison to a single index file
6 | like in Sphinx_ this has several advantages:
7 |
8 | - :math:`O(\frac{1}{27} n \log n)` instead of :math:`O(n)` space efficiency
9 | - full text search with no arbitrary character set limitation
10 | - exact and partial matches in :math:`O(m) + O(k \log n)`
11 |
12 | .. For the record (index for around 170 posts):
13 |
14 | .. - JSON index with no special characters allowed (such as dash): 375k (132k gzipped)
15 | .. - CST index with special characters (except punctuation): 42k (12k gzipped) per prefix
16 |
17 | So, what is a prefix? The idea is, that a user does mostly search for a
18 | single keyword a single time and maybe with a refinement afterwards. The user
19 | does not need to load the whole site index just to query for "python" or
20 | "python project". With (compressed) suffix trees, it is possible to split
21 | the index into one-character prefixes; in the example, `p` for `python` and
22 | fortunately even for `project`. Acrylamid can construct a CST in
23 | :math:`O(n \log n)` for a constant size alphabet. The alphabet use 26
24 | lowercase ascii characters and a tree for everything else, hence
25 | :math:`O(\frac{1}{27} n \log n)` space efficiency per sub tree. In practice
26 | (due tree compression and repitive language) this is more space-efficient
27 | than a global index (42k versus 375k in average for 170 posts).
28 |
29 | Like Sphinx_ the index only links to the article containing the keyword and
30 | does not provide any context. Hence, the search view renders a plain text
31 | version of all posts and the API provides a method to get up to N paragraphs
32 | containing the keyword (loaded asynchronously in background).
33 |
34 | .. _suffix trees: https://en.wikipedia.org/wiki/Suffix_tree
35 | .. _Sphinx: http://sphinx-doc.org/
36 |
37 | Usage
38 | -----
39 |
40 | Note, that the new default theme includes the static site search by default.
41 | Just enable the search view in your configuration and point to a directory
42 | where you would like to store the index.
43 |
44 | .. code-block:: python
45 |
46 | '/search/': {'view': 'search', 'filters': 'strip+pre'} # ignores tag
47 |
48 | The search view does not run by default because the construction of a compressed
49 | suffix tree is very expensive. Hence, supply ``--search`` to build the index.
50 |
51 | .. code-block:: sh
52 |
53 | $ acrylamid compile --search
54 |
55 | You can query the CST as shown in the example below. You still have to
56 | highlight the keyword yourself. The API only returns you the article and
57 | optional a context.
58 |
59 | .. code-block:: html
60 |
61 |
62 |
63 |
78 |
79 |
80 |
83 |
84 |
85 | Javascript API
86 | --------------
87 |
88 | .. js:function:: search(keyword)
89 |
90 | :param keyword: keyword to search for
91 | :returns: a tuple of exact matches and partial matches as array or
92 | undefined if not found
93 |
94 | .. js:function:: search.context(keyword, id[, limit=1])
95 |
96 | :param keyword: keyword used for :js:func:`search`
97 | :param int id: id from search result
98 | :param int limit: limit context to N paragraphs
99 | :returns: a list of paragraphs containing the keyword
100 |
101 | .. js:attribute:: search.lookup
102 |
103 | An id to entry mapping. Use the ids from :js:func:`search` to get a tuple
104 | containing the (relative) permalink and title back.
105 |
106 | .. js:attribute:: search.path
107 |
108 | Relative location of the search index.
109 |
--------------------------------------------------------------------------------
/acrylamid/hooks.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2013 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | import os
7 | import io
8 | import re
9 | import types
10 | import shutil
11 | import multiprocessing
12 |
13 | from os.path import isfile, getmtime, isdir, dirname
14 | from tempfile import mkstemp
15 | from functools import partial
16 |
17 | from acrylamid import log
18 | from acrylamid.errors import AcrylamidException
19 | from acrylamid.compat import string_types, iteritems
20 |
21 | from acrylamid.helpers import event, system, discover
22 | from acrylamid.lib._async import Threadpool
23 |
24 | pool = None
25 | tasks = {}
26 |
27 |
28 | def modified(src, dest):
29 | return not isfile(dest) or getmtime(src) > getmtime(dest)
30 |
31 |
32 | def execute(cmd, ns, src, dest=None):
33 | """Execute `cmd` such as `yui-compressor %1 -o %2` in-place.
34 | If `dest` is none, you don't have to supply %2."""
35 |
36 | assert '%1' in cmd
37 | cmd = cmd.replace('%1', src)
38 |
39 | if dest:
40 | assert '%2' in cmd
41 | cmd = cmd.replace('%2', dest)
42 |
43 | if not isdir(dirname(dest)):
44 | os.makedirs(dirname(dest))
45 |
46 | try:
47 | rv = system(cmd, shell=True)
48 | except (AcrylamidException, OSError):
49 | log.exception("uncaught exception during execution")
50 | return
51 |
52 | if dest is None:
53 | fd, path = mkstemp()
54 | with io.open(fd, 'w', encoding='utf-8') as fp:
55 | fp.write(rv)
56 | shutil.move(path, src)
57 | log.info('update %s', src)
58 | else:
59 | log.info('create %s', dest)
60 |
61 |
62 | def simple(pool, pattern, normalize, action, ns, path):
63 | """
64 | :param pool: threadpool
65 | :param pattern: if pattern matches `path`, queue action
66 | :param action: task to run
67 | """
68 | if re.match(pattern, normalize(path), re.I):
69 | if isinstance(action, string_types):
70 | action = partial(execute, action)
71 | pool.add_task(action, ns, path)
72 |
73 |
74 | def advanced(pool, pattern, force, normalize, action, translate, ns, path):
75 | """
76 | :param force: re-run task even when the source has not been modified
77 | :param pattern: a regular expression to match the original path
78 | :param func: function to run
79 | :param translate: path translation, e.g. /images/*.jpg -> /images/thumbs/*.jpg
80 | """
81 | if not re.match(pattern, normalize(path), re.I):
82 | return
83 |
84 | if force or modified(path, translate(path)):
85 | if isinstance(action, string_types):
86 | action = partial(execute, action)
87 | pool.add_task(action, ns, path, translate(path))
88 | else:
89 | log.skip('skip %s', translate(path))
90 |
91 |
92 | def pre(func):
93 | global tasks
94 | tasks.setdefault('pre', []).append(func)
95 |
96 |
97 | def post(func):
98 | global tasks
99 | tasks.setdefault('post', []).append(func)
100 |
101 |
102 | def run(conf, env, type):
103 |
104 | global tasks
105 |
106 | pool = Threadpool(multiprocessing.cpu_count())
107 | while tasks.get(type):
108 | pool.add_task(partial(tasks[type].pop(), conf, env))
109 |
110 | pool.wait_completion()
111 |
112 |
113 | def initialize(conf, env):
114 |
115 | global pool
116 |
117 | hooks, blocks = conf.get('hooks', {}), not conf.get('hooks_mt', True)
118 | pool = Threadpool(1 if blocks else multiprocessing.cpu_count(), wait=blocks)
119 |
120 | force = env.options.force
121 | normalize = lambda path: path.replace(conf['output_dir'], '')
122 |
123 | for pattern, action in iteritems(hooks):
124 | if isinstance(action, (types.FunctionType, string_types)):
125 | event.register(
126 | callback=partial(simple, pool, pattern, normalize, action),
127 | to=['create', 'update'] if not force else event.events)
128 | else:
129 | event.register(
130 | callback=partial(advanced, pool, pattern, force, normalize, *action),
131 | to=event.events)
132 |
133 | discover([conf.get('HOOKS_DIR', 'hooks/')], lambda x: x)
134 |
135 |
136 | def shutdown():
137 |
138 | global pool
139 | pool.wait_completion()
140 |
--------------------------------------------------------------------------------
/specs/readers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals
4 |
5 | import io
6 | import attest
7 |
8 | tt = attest.Tests()
9 | from acrylamid.readers import reststyle, markdownstyle, distinguish, ignored
10 | from acrylamid.readers import pandocstyle
11 |
12 |
13 | @tt.test
14 | def rest():
15 |
16 | header = ["Header",
17 | "======",
18 | "",
19 | ":date: 2001-08-16",
20 | ":version: 1",
21 | ":draft: True",
22 | ":authors: foo, bar",
23 | ":indentation: Since the field marker may be quite long, the second",
24 | " and subsequent lines of the field body do not have to line up",
25 | " with the first line, but they must be indented relative to the",
26 | " field name marker, and they must line up with each other.",
27 | ":parameter i: integer",
28 | "",
29 | "Hello *World*."]
30 |
31 | i, meta = reststyle(io.StringIO('\n'.join(header)))
32 | assert i == len(header) - 1
33 |
34 | assert 'foo' in meta['authors']
35 | assert meta['version'] == 1
36 | assert meta['date'] == '2001-08-16'
37 | assert 'second and subsequent' in meta['indentation']
38 | assert meta['draft'] is True
39 |
40 |
41 | @tt.test
42 | def mkdown():
43 |
44 | header = ["Title: My Document",
45 | "Summary: A brief description of my document.",
46 | "Authors: Waylan Limberg",
47 | " John Doe",
48 | "Date: October 2, 2007",
49 | "blank-value: ",
50 | "base_url: http://example.com",
51 | "",
52 | "This is the first paragraph of the document."]
53 |
54 | i, meta = markdownstyle(io.StringIO('\n'.join(header)))
55 | assert i == len(header) - 1
56 |
57 | assert 'John Doe' in meta['authors']
58 | assert meta['date'] == 'October 2, 2007'
59 | assert meta['blank-value'] == ""
60 |
61 |
62 | @tt.test
63 | def pandoc():
64 |
65 | header = ["% title",
66 | "% Author; Another",
67 | "% June 15, 2006",
68 | "",
69 | "Here comes the regular text"]
70 |
71 | i, meta = pandocstyle(io.StringIO('\n'.join(header)))
72 | assert i == len(header) - 1
73 |
74 | assert 'Another' in meta['author']
75 | assert meta['date'] == 'June 15, 2006'
76 |
77 |
78 | @tt.test
79 | def quotes():
80 |
81 | assert distinguish('"') == '"'
82 | assert distinguish('""') == ''
83 |
84 | assert distinguish('Foo"') == 'Foo"'
85 | assert distinguish('"Foo') == '"Foo'
86 |
87 | assert distinguish('"Foo" Bar') == '"Foo" Bar'
88 | assert distinguish('"Foo Bar"') == 'Foo Bar'
89 |
90 | assert distinguish("\"'bout \" and '\"") == "'bout \" and '"
91 |
92 | # quote commas, so they are not recognized as a new part
93 | assert distinguish('["X+ext(foo, bar=123)", other]') == ["X+ext(foo, bar=123)", "other"]
94 | assert distinguish('["a,b,c,d", a, b, c]') == ['a,b,c,d', 'a', 'b', 'c']
95 |
96 | # shlex tokenizer should not split on "+" and " "
97 | assert distinguish("[X+Y]") == ["X+Y"]
98 | assert distinguish("[foo bar, baz]") == ["foo bar", "baz"]
99 | assert distinguish("[Foo, ]") == ["Foo"]
100 |
101 | # non-ascii
102 | assert distinguish('["Föhn", "Bär"]') == ["Föhn", "Bär"]
103 | assert distinguish('[Bla, Calléjon]') == ["Bla", "Calléjon"]
104 | assert distinguish('[да, нет]') == ["да", "нет"]
105 |
106 |
107 | @tt.test
108 | def types():
109 |
110 | for val in ['None', 'none', '~', 'null']:
111 | assert distinguish(val) == None
112 |
113 | for val in ['3.14', '42.0', '-0.01']:
114 | assert distinguish(val) == float(val)
115 |
116 | for val in ['1', '2', '-1', '9000']:
117 | assert distinguish(val) == int(val)
118 |
119 | assert distinguish('test') == 'test'
120 | assert distinguish('') == ''
121 |
122 |
123 | @tt.test
124 | def backslash():
125 |
126 | assert distinguish('\\_bar') == '_bar'
127 | assert distinguish('foo\\_') == 'foo_'
128 | assert distinguish('foo\\\\bar') == 'foo\\bar'
129 |
130 |
131 | @tt.test
132 | def ignore():
133 |
134 | assert ignored('/path/', 'foo', ['foo', 'fo*', '/foo'], '/path/')
135 | assert ignored('/path/', 'dir/', ['dir', 'dir/'], '/path/')
136 | assert not ignored('/path/to/', 'baz/', ['/baz/', '/baz'], '/path/')
137 |
138 | assert ignored('/', '.git/info/refs', ['.git*'], '/')
139 | assert ignored('/', '.gitignore', ['.git*'], '/')
140 |
141 | assert ignored('/', '.DS_Store', ['.DS_Store'], '/')
142 |
--------------------------------------------------------------------------------
/acrylamid/views/tag.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | import math
7 | import random
8 |
9 | from collections import defaultdict
10 |
11 | from acrylamid.compat import iteritems
12 | from acrylamid.helpers import expand, safeslug, hash
13 | from acrylamid.views.index import Index, Paginator
14 |
15 |
16 | def fetch(entrylist):
17 | """Fetch tags from list of entries and map tags to most common tag name
18 | """
19 | tags = defaultdict(list)
20 | tmap = defaultdict(int)
21 |
22 | for e in entrylist:
23 | for tag in e.tags:
24 | tags[tag.lower()].append(e)
25 | tmap[tag] += 1
26 |
27 | # map tags to the most counted tag name
28 | for name in list(tags.keys()):
29 | key = max([(tmap[key], key) for key in tmap
30 | if key.lower() == name])[1]
31 | rv = tags.pop(key.lower())
32 | tags[key] = rv
33 |
34 | return tags
35 |
36 |
37 | class Tagcloud(object):
38 | """Tagcloud helper class similar (almost identical) to pelican's tagcloud helper object.
39 | Takes a bunch of tags and produces a logarithm-based partition and returns a iterable
40 | object yielding a Tag-object with two attributes: name and step where step is the
41 | calculated step size (== font size) and reaches from 0 to steps-1.
42 |
43 | :param tags: a dictionary of tags, e.g. {'name', [list of entries]}
44 | :param steps: maximum steps
45 | :param max_items: maximum items shown in tagcloud
46 | :param start: start index of steps resulting in start to steps+start-1 steps."""
47 |
48 | def __init__(self, tags, steps=4, max_items=100, start=0, shuffle=False):
49 |
50 | lst = sorted([(k, len(v)) for k, v in iteritems(tags)],
51 | key=lambda x: x[0])[:max_items]
52 | # stolen from pelican/generators.py:286
53 | max_count = max(lst, key=lambda k: k[1])[1] if lst else None
54 | self.lst = [(tag, count,
55 | int(math.floor(steps - (steps - 1) * math.log(count)
56 | / (math.log(max_count) or 1)))+start-1)
57 | for tag, count in lst]
58 |
59 | if shuffle:
60 | random.shuffle(self.lst)
61 |
62 | self.tags = tags
63 |
64 | def __iter__(self):
65 | for tag, count, step in self.lst:
66 | yield type('Tag', (), {'name': tag, 'step': step, 'count': count})
67 |
68 | def __hash__(self):
69 | return hash(*self.lst)
70 |
71 | def __getitem__(self, tag):
72 | return self.tags[tag.name]
73 |
74 |
75 | class Tag(Index):
76 | """Same behaviour like Index except ``route`` that defaults to */tag/:name/* and
77 | ``pagination`` that defaults to */tag/:name/:num/* where :name is the current
78 | tag identifier.
79 |
80 | To create a tag cloud head over to :doc:`conf.py`.
81 | """
82 |
83 | export = ['prev', 'curr', 'next', 'items_per_page', 'tag', 'entrylist']
84 | template = 'main.html'
85 |
86 | def populate_tags(self, request):
87 |
88 | tags = fetch(request['entrylist'])
89 | self.tags = tags
90 | return tags
91 |
92 | def context(self, conf, env, request):
93 |
94 | class Link:
95 |
96 | def __init__(self, title, href):
97 | self.title = title
98 | self.href = href
99 |
100 | def tagify(tags):
101 | href = lambda t: expand(self.path, {'name': safeslug(t)})
102 | return [Link(t, href(t)) for t in tags] if isinstance(tags, (list, tuple)) \
103 | else Link(tags, href(tags))
104 |
105 | tags = self.populate_tags(request)
106 | env.engine.register('tagify', tagify)
107 | env.tag_cloud = Tagcloud(tags, conf['tag_cloud_steps'],
108 | conf['tag_cloud_max_items'],
109 | conf['tag_cloud_start_index'],
110 | conf['tag_cloud_shuffle'])
111 |
112 | return env
113 |
114 | def generate(self, conf, env, data):
115 | """Creates paged listing by tag."""
116 |
117 | for tag in self.tags:
118 |
119 | data['entrylist'] = [entry for entry in self.tags[tag]]
120 | for res in Paginator.generate(self, conf, env, data, tag=tag, name=safeslug(tag)):
121 | yield res
122 |
--------------------------------------------------------------------------------
/acrylamid/views/feeds.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | from os.path import isfile
7 | from datetime import datetime, timedelta
8 | from wsgiref.handlers import format_date_time
9 |
10 | from acrylamid.utils import HashableList, total_seconds
11 | from acrylamid.views import View, tag
12 | from acrylamid.compat import text_type as str
13 | from acrylamid.helpers import joinurl, event, expand, union
14 | from acrylamid.readers import Timezone
15 |
16 | epoch = datetime.utcfromtimestamp(0).replace(tzinfo=Timezone(0))
17 |
18 |
19 | def utc(dt, fmt='%Y-%m-%dT%H:%M:%SZ'):
20 | """return date pre-formated as UTC timestamp.
21 | """
22 | return (dt - (dt.utcoffset() or timedelta())).strftime(fmt)
23 |
24 |
25 | class Feed(View):
26 | """Atom and RSS feed generation. The feeds module provides several classes
27 | to generate feeds:
28 |
29 | - RSS -- RSS feed for all entries
30 | - Atom -- same for Atom
31 | - RSSPerTag -- RSS feed for all entries for a given tag
32 | - AtomPerTag -- same for Atom
33 |
34 | All feed views have a ``num_entries`` argument that defaults to 25 and
35 | limits the list of posts to the 25 latest ones. In addition RSSPerTag and
36 | AtomPerTag expand ``:name`` to the current tag in your route.
37 |
38 | Examples:
39 |
40 | .. code-block:: python
41 |
42 | # per tag Atom feed
43 | '/tag/:name/feed/': {'filters': ..., 'view': 'atompertag'}
44 |
45 | # full Atom feed
46 | '/atom/full/': {'filters': ..., 'view': 'atom', 'num_entries': 1000}
47 | """
48 |
49 | priority = 25.0
50 |
51 | def init(self, conf, env):
52 | self.filters.append('absolute')
53 | self.route = self.path
54 |
55 | def context(self, conf, env, data):
56 | env.engine.register('utc', utc)
57 | return env
58 |
59 | def generate(self, conf, env, data):
60 | entrylist = data['entrylist']
61 | entrylist = list(entrylist)[0:self.num_entries]
62 | tt = env.engine.fromfile(env, '%s.xml' % self.type)
63 |
64 | path = joinurl(conf['output_dir'], self.route)
65 | modified = any(entry.modified for entry in entrylist)
66 |
67 | if isfile(path) and not (conf.modified or env.modified or tt.modified or modified):
68 | event.skip(self.name, path)
69 | raise StopIteration
70 |
71 | updated = entrylist[0].date if entrylist \
72 | else datetime.utcnow().replace(tzinfo=conf.tzinfo)
73 | html = tt.render(conf=conf, env=union(env, route=self.route,
74 | updated=updated, entrylist=entrylist))
75 | yield html, path
76 |
77 |
78 | class FeedPerTag(tag.Tag, Feed):
79 |
80 | def context(self, conf, env, data):
81 | self.populate_tags(data)
82 |
83 | return env
84 |
85 | def generate(self, conf, env, data):
86 |
87 | for tag in self.tags:
88 |
89 | entrylist = HashableList(entry for entry in self.tags[tag])
90 | new_data = data
91 | new_data['entrylist'] = entrylist
92 | self.route = expand(self.path, {'name': tag})
93 | for html, path in Feed.generate(self, conf, env, new_data):
94 | yield html, path
95 |
96 |
97 | class Atom(Feed):
98 |
99 | def init(self, conf, env, num_entries=25):
100 | super(Atom, self).init(conf, env)
101 |
102 | self.num_entries = num_entries
103 | self.type = 'atom'
104 |
105 |
106 | class RSS(Feed):
107 |
108 | def init(self, conf, env, num_entries=25):
109 | super(RSS, self).init(conf, env)
110 |
111 | self.num_entries = num_entries
112 | env.engine.register(
113 | 'rfc822', lambda dt: str(format_date_time(total_seconds(dt - epoch))))
114 | self.type = 'rss'
115 |
116 |
117 | class AtomPerTag(FeedPerTag):
118 |
119 | def init(self, conf, env, num_entries=25):
120 | super(AtomPerTag, self).init(conf, env)
121 |
122 | self.num_entries = num_entries
123 | self.type = 'atom'
124 |
125 |
126 | class RssPerTag(FeedPerTag):
127 |
128 | def init(self, conf, env, num_entries=25):
129 | super(RssPerTag, self).init(conf, env)
130 |
131 | self.num_entries = num_entries
132 | env.engine.register(
133 | 'rfc822', lambda dt: str(format_date_time(total_seconds(dt - epoch))))
134 | self.type = 'rss'
135 |
--------------------------------------------------------------------------------
/acrylamid/views/sitemap.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2012 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | import io
7 |
8 | from time import strftime, gmtime
9 | from os.path import getmtime, exists, splitext, basename
10 | from xml.sax.saxutils import escape
11 |
12 | from acrylamid.views import View
13 | from acrylamid.compat import PY2K
14 | from acrylamid.helpers import event, joinurl, rchop
15 |
16 | if PY2K:
17 | from urlparse import urljoin
18 | else:
19 | from urllib.parse import urljoin
20 |
21 |
22 | class Map(io.StringIO):
23 | """A simple Sitemap generator."""
24 |
25 | def __init__(self, *args, **kw):
26 |
27 | io.StringIO.__init__(self)
28 | self.write(u"\n")
29 | self.write(u'\n')
31 |
32 | def add(self, url, lastmod, changefreq='never', priority=0.5, images=None):
33 |
34 | self.write(u' \n')
35 | self.write(u' %s \n' % escape(url))
36 | self.write(u' %s \n' % strftime('%Y-%m-%d', gmtime(lastmod)))
37 | if changefreq:
38 | self.write(u' %s \n' % changefreq)
39 | if priority != 0.5:
40 | self.write(u' %.1f \n' % priority)
41 | for img in images or []:
42 | self.write(u' \n')
43 | self.write(u' %s \n' % escape(urljoin(url, basename(img))))
44 | self.write(u' \n')
45 |
46 | self.write(u' \n')
47 |
48 | def finish(self):
49 | self.write(u' ')
50 |
51 |
52 | class Sitemap(View):
53 |
54 | priority = 0.0
55 | scores = {'page': (1.0, 'never'), 'entry': (1.0, 'never')}
56 |
57 | def init(self, conf, env):
58 |
59 | def track(ns, path):
60 | if ns != 'resource':
61 | self.files.add((ns, path))
62 | elif self.resext and splitext(path)[1] in self.resext:
63 | self.files.add((ns, path))
64 |
65 | def changed(ns, path):
66 | if not self.modified:
67 | self.modified = True
68 |
69 | self.files = set([])
70 | self.modified = False
71 |
72 | # use extension to check if resource should be tracked (keep image, video and other resources separate)
73 | self.resext = conf.get('sitemap_resource_ext', [])
74 | self.imgext = conf.get('sitemap_image_ext', [])
75 | # video resources require more attributes (image, description)
76 | # see http://support.google.com/webmasters/bin/answer.py?hl=en&answer=183668
77 | #self.vidext = conf.get('sitemap_video_ext', [])
78 |
79 | # track output files
80 | event.register(track, to=['create', 'update', 'skip', 'identical'])
81 | event.register(changed, to=['create', 'update'])
82 |
83 | def context(self, conf, env, data):
84 | """If resources are included in sitemap, create a map for each entry and its
85 | resources, so they can be include in """
86 |
87 | if self.imgext:
88 | self.mapping = dict([(entry.permalink, entry.resources)
89 | for entry in data['entrylist']])
90 |
91 | return env
92 |
93 | def generate(self, conf, env, data):
94 | """In this step, we filter drafted entries (they should not be included into the
95 | Sitemap) and write the pre-defined priorities to the map."""
96 |
97 | path = joinurl(conf['output_dir'], self.path)
98 | sm = Map()
99 |
100 | if exists(path) and not self.modified and not conf.modified:
101 | event.skip('sitemap', path)
102 | raise StopIteration
103 |
104 | for ns, fname in self.files:
105 |
106 | if ns == 'draft':
107 | continue
108 |
109 | permalink = '/' + fname.replace(conf['output_dir'], '')
110 | permalink = rchop(permalink, 'index.html')
111 | url = conf['www_root'] + permalink
112 | priority, changefreq = self.scores.get(ns, (0.5, 'weekly'))
113 | if self.imgext:
114 | images = [x for x in self.mapping.get(permalink, []) if splitext(x)[1].lower() in self.imgext]
115 | sm.add(url, getmtime(fname), changefreq, priority, images)
116 | else:
117 | sm.add(url, getmtime(fname), changefreq, priority)
118 | sm.finish()
119 | yield sm, path
120 |
--------------------------------------------------------------------------------
/docs/filters/markup/md.rst:
--------------------------------------------------------------------------------
1 | Markdown (and Variants)
2 | =======================
3 |
4 | Markdown is a text-to-HTML conversion tool for web writers. Markdown allows
5 | you to write using an easy-to-read, easy-to-write plain text format, then
6 | convert it to structurally valid XHTML (or HTML). -- `John Gruber`_
7 |
8 | Here's an online service converting Markdown to HTML and providing a handy
9 | cheat sheet: `Dingus `_.
10 |
11 | .. _John Gruber: http://daringfireball.net/projects/markdown/
12 |
13 | Implementation
14 | --------------
15 |
16 | There are many different Markdown implementations and extensions. Acrylamid
17 | uses `Python Markdown`_ as primary library to parse, compile and extend
18 | Markdown. The library is almost compliant with the reference implementation,
19 | but has a few very `minor differences`_.
20 |
21 | You can write your posts with a so called "meta data" header; an unofficial
22 | extension used by the popular MultiMarkdown_ and `Python Markdown`_ (keys are
23 | case-insensitive and converted to lowercase):
24 |
25 | .. code-block:: text
26 |
27 | Title: A Sample MultiMarkdown Document
28 | Author: Fletcher T. Penney
29 | Date: Feb 9, 2011
30 | Comment: This is a comment intended to demonstrate
31 | metadata that spans multiple lines, yet
32 | is treated as a single value.
33 | Test: And this is a new key-value pair
34 | Tags: [Are, parsed, YAML-like]
35 |
36 | .. _Python Markdown: http://pythonhosted.org/Markdown/
37 | .. _minor differences: http://pythonhosted.org/Markdown/#differences
38 | .. _MultiMarkdown: http://fletcherpenney.net/multimarkdown/
39 |
40 | Usage
41 | ^^^^^
42 |
43 | ============ ====================================================
44 | Requires ``markdown`` or (``python-markdown``) -- already
45 | as a dependency implicitly installed
46 | Aliases md, mkdown, markdown
47 | Conflicts HTML, reStructuredText, Pandoc
48 | Arguments asciimathml, sub, sup, delins, gist, gistraw
49 |
50 | ============ ====================================================
51 |
52 | Extensions
53 | ^^^^^^^^^^
54 |
55 | `Python Markdown`_ ships with a few popular extensions such as ``tables``,
56 | see their `supported extensions` page for details. You can add extensions
57 | selectively via ``markdown+tables+...`` or add the most common extensions
58 | via ``markdown+extras``.
59 |
60 | In addition to the official extensions, Acrylamid features
61 |
62 | * inline math via AsciiMathML_. The aliases are: *asciimathml*, *mathml* and
63 | *math* and require the ``python-asciimathml`` package. *Note* put your formula
64 | into single dollar signs like ``$a+b^2$``!
65 | * super_ und subscript_ via *sup* (or *superscript*) and *sub* (or
66 | *subscript*). The syntax for subscript is ``H~2~O`` and for superscript
67 | ``a^2^``.
68 | * `deletion and insertion`_ syntax via *delins*. The syntax is ``~~old~~`` and
69 | ``++new++``.
70 | * `GitHub:Gist `__ embedding via ``[gist: id]`` and
71 | optional with a filename ``[gist: id filename]``. You can use ``[gistraw:
72 | id [filename]]`` to embed the raw text without JavaScript.
73 |
74 | out-of-the-box.
75 |
76 | .. _supported extensions: http://pythonhosted.org/Markdown/extensions/
77 | .. _AsciiMathML: https://github.com/favalex/python-asciimathml
78 | .. _super: https://github.com/sgraber/markdown.superscript
79 | .. _subscript: https://github.com/sgraber/markdown.subscript
80 | .. _deletion and insertion: https://github.com/aleray/mdx_del_ins
81 |
82 | Limitations
83 | ^^^^^^^^^^^
84 |
85 | Markdown is very easy to write, but especially as a developer, you might
86 | experience some rare "issues" not covered by the official test suite, for
87 | example it is impossible to write an unordered list followed by a code
88 | listing. Read `Pandoc's Markdown`_ to see what's "wrong" or difficult to
89 | achieve in markdown.
90 |
91 | .. _Pandoc's Markdown: http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown
92 |
93 | Discount
94 | --------
95 |
96 | `Discount`__ -- a C implementation of John Gruber's Markdown including
97 | definition lists, pseudo protocols and `Smartypants`__ (makes
98 | :ref:`filters-post-typography` obsolete).
99 |
100 | __ http://www.pell.portland.or.us/~orc/Code/discount/#smartypants
101 | __ http://www.pell.portland.or.us/~orc/Code/discount/
102 |
103 |
104 | ============ =========================================================
105 | Requires `discount `_
106 | Aliases Discount, discount
107 | Conflicts reStructuredText, Markdown, Pandoc, PyTextile, Typography
108 | ============ =========================================================
109 |
--------------------------------------------------------------------------------
/specs/helpers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import attest
4 |
5 | from acrylamid import helpers, refs
6 | from acrylamid import AcrylamidException
7 |
8 |
9 | class Helpers(attest.TestBase):
10 |
11 | @attest.test
12 | def safeslug(self):
13 |
14 | examples = (('This is a Test', 'this-is-a-test'),
15 | ('this is a test', 'this-is-a-test'),
16 | ('This is another-- test', 'this-is-another-test'),
17 | ('A real example: Hello World in C++ -- "a new approach*"!',
18 | 'a-real-example-hello-world-in-c++-a-new-approach'))
19 |
20 | for value, expected in examples:
21 | assert helpers.safeslug(value) == expected
22 |
23 | examples = ((u'Hänsel und Gretel', 'haensel-und-gretel'),
24 | (u'fácil € ☺', 'facil-eu'),
25 | (u'русский', 'russkii'))
26 |
27 | for value, expected in examples:
28 | assert helpers.safeslug(value) == expected
29 |
30 | @attest.test
31 | def joinurl(self):
32 |
33 | examples = ((['hello', 'world'], 'hello/world'),
34 | (['/hello', 'world'], '/hello/world'),
35 | (['hello', '/world'], 'hello/world'),
36 | (['/hello', '/world'], '/hello/world'),
37 | (['/hello/', '/world/'], '/hello/world/index.html'),
38 | (['/bar/', '/'], '/bar/index.html'))
39 |
40 | for value, expected in examples:
41 | assert helpers.joinurl(*value) == expected
42 |
43 | @attest.test
44 | def expand(self):
45 |
46 | assert helpers.expand('/:foo/:bar/', {'foo': 1, 'bar': 2}) == '/1/2/'
47 | assert helpers.expand('/:foo/:spam/', {'foo': 1, 'bar': 2}) == '/1/spam/'
48 | assert helpers.expand('/:foo/', {'bar': 2}) == '/foo/'
49 |
50 | assert helpers.expand('/:slug.html', {'slug': 'foo'}) == '/foo.html'
51 | assert helpers.expand('/:slug.:slug.html', {'slug': 'foo'}) == '/foo.foo.html'
52 |
53 | @attest.test
54 | def paginate(self):
55 |
56 | X = type('X', (str, ), {'modified': True}); refs.load()
57 |
58 | res = ['1', 'asd', 'asd123', 'egg', 'spam', 'ham', '3.14', '42']
59 | res = [X(val) for val in res]
60 |
61 | # default stuff
62 | assert list(helpers.paginate(res, 4)) == \
63 | [((None, 1, 2), res[:4], True), ((1, 2, None), res[4:], True)]
64 | assert list(helpers.paginate(res, 7)) == \
65 | [((None, 1, 2), res[:7], True), ((1, 2, None), res[7:], True)]
66 |
67 | # with orphans
68 | assert list(helpers.paginate(res, 7, orphans=1)) == \
69 | [((None, 1, None), res, True)]
70 | assert list(helpers.paginate(res, 6, orphans=1)) == \
71 | [((None, 1, 2), res[:6], True), ((1, 2, None), res[6:], True)]
72 |
73 | # a real world example which has previously failed
74 | res = [X(_) for _ in range(20)]
75 | assert list(helpers.paginate(res, 10)) == \
76 | [((None, 1, 2), res[:10], True), ((1, 2, None), res[10:], True)]
77 |
78 | res = [X(_) for _ in range(21)]
79 | assert list(helpers.paginate(res, 10)) == \
80 | [((None, 1, 2), res[:10], True), ((1, 2, 3), res[10:20], True),
81 | ((2, 3, None), res[20:], True)]
82 |
83 | # edge cases
84 | assert list(helpers.paginate([], 2)) == []
85 | assert list(helpers.paginate([], 2, orphans=7)) == []
86 | assert list(helpers.paginate([X('1'), X('2'), X('3')], 3, orphans=1)) == \
87 | [((None, 1, None), [X('1'), X('2'), X('3')], True)]
88 |
89 | @attest.test
90 | def safe(self):
91 |
92 | assert helpers.safe('"') == '"'
93 | assert helpers.safe('') == '""'
94 |
95 | assert helpers.safe('*Foo') == '"*Foo"'
96 | assert helpers.safe('{"Foo') == '\'{"Foo\''
97 |
98 | assert helpers.safe('"Foo" Bar') == '"Foo" Bar'
99 | assert helpers.safe("'bout \" and '") == "\"'bout \" and '\""
100 |
101 | assert helpers.safe('Hello World') == 'Hello World'
102 | assert helpers.safe('Hello: World') == '"Hello: World"'
103 | assert helpers.safe('Hello\'s World') == 'Hello\'s World'
104 | assert helpers.safe('Hello "World"') == 'Hello "World"'
105 |
106 | assert helpers.safe('[foo][bar] Baz') == '"[foo][bar] Baz"'
107 |
108 | @attest.test
109 | def system(self):
110 |
111 | examples = ((['echo', 'ham'], None, 'ham'),
112 | ('cat', 'foo', 'foo'),
113 | )
114 | for cmd, stdin, expected in examples:
115 | assert helpers.system(cmd, stdin) == expected
116 |
117 | with attest.raises(AcrylamidException):
118 | helpers.system('false')
119 |
120 | with attest.raises(OSError):
121 | helpers.system('foo', None)
122 |
--------------------------------------------------------------------------------
/acrylamid/assets/web.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright 2013 Martin Zimmermann . All rights reserved.
4 | # License: BSD Style, 2 clauses -- see LICENSE.
5 |
6 | import os
7 |
8 | from os.path import join, isdir, dirname, getmtime, relpath
9 | from itertools import chain
10 |
11 | from acrylamid import core
12 | from acrylamid.compat import map
13 |
14 | from acrylamid.utils import cached_property
15 | from acrylamid.helpers import event
16 |
17 | from webassets.env import Environment, Resolver
18 | from webassets.merge import FileHunk
19 | from webassets.bundle import Bundle, has_placeholder
20 | from webassets.updater import TimestampUpdater
21 | from webassets.version import HashVersion
22 |
23 |
24 | class Acrylresolver(Resolver):
25 |
26 | def __init__(self, conf, environment):
27 | super(Acrylresolver, self).__init__(environment)
28 | self.conf = conf
29 |
30 | def resolve_output_to_path(self, target, bundle):
31 |
32 | if not target.startswith(self.conf.output_dir):
33 | target = join(self.conf.output_dir, target)
34 |
35 | if not isdir(dirname(target)):
36 | os.makedirs(dirname(target))
37 |
38 | return target
39 |
40 |
41 | class Acrylupdater(TimestampUpdater):
42 | """Keep incremental compilation even with ``depends``, which is currently
43 | not provided by webassets: https://github.com/miracle2k/webassets/pull/220.
44 | """
45 | id = 'acrylic'
46 | used, new = set(), set()
47 |
48 | def build_done(self, bundle, env):
49 | func = event.create if bundle in self.new else event.update
50 | func('webassets', bundle.resolve_output(env))
51 | return super(Acrylupdater, self).build_done(bundle, env)
52 |
53 | def needs_rebuild(self, bundle, env):
54 |
55 | if super(TimestampUpdater, self).needs_rebuild(bundle, env):
56 | return True
57 |
58 | try:
59 | dest = getmtime(bundle.resolve_output(env))
60 | except OSError:
61 | return self.new.add(bundle) or True
62 |
63 | src = [s[1] for s in bundle.resolve_contents(env)]
64 | deps = bundle.resolve_depends(env)
65 |
66 | for item in src + deps:
67 | self.used.add(item)
68 |
69 | if any(getmtime(deps) > dest for deps in src + deps):
70 | return True
71 |
72 | event.skip('assets', bundle.resolve_output(env))
73 |
74 | return False
75 |
76 |
77 | class Acrylversion(HashVersion):
78 | """Hash based on the input (+ depends), not on the output."""
79 |
80 | id = 'acrylic'
81 |
82 | def determine_version(self, bundle, env, hunk=None):
83 |
84 | if not hunk and not has_placeholder(bundle.output):
85 | hunks = [FileHunk(bundle.resolve_output(env)), ]
86 | elif not hunk:
87 | src = sum(map(env.resolver.resolve_source, bundle.contents), [])
88 | hunks = [FileHunk(hunk) for hunk in src + bundle.resolve_depends(env)]
89 | else:
90 | hunks = [hunk, ]
91 |
92 | hasher = self.hasher()
93 | for hunk in hunks:
94 | hasher.update(hunk.data())
95 | return hasher.hexdigest()[:self.length]
96 |
97 |
98 | class Webassets(object):
99 |
100 | def __init__(self, conf, env):
101 | self.conf = conf
102 | self.env = env
103 |
104 | self.environment = Environment(
105 | directory=conf.theme[0], url=env.path,
106 | updater='acrylic', versions='acrylic',
107 | cache=core.cache.cache_dir, load_path=[conf.theme[0]])
108 |
109 | # fix output directory creation
110 | self.environment.resolver = Acrylresolver(conf, self.environment)
111 |
112 | def excludes(self, directory):
113 | """Return used assets relative to :param:`directory`."""
114 | return [relpath(p, directory) for p in self.environment.updater.used]
115 |
116 | def compile(self, *args, **kw):
117 |
118 | assert 'output' in kw
119 | kw.setdefault('debug', False)
120 |
121 | bundle = Bundle(*args, **kw)
122 | for url in bundle.urls(env=self.environment):
123 | yield url
124 |
125 |
126 | class Mixin:
127 |
128 | @cached_property
129 | def modified(self):
130 | """Iterate template dependencies for modification and check web assets
131 | if a bundle needs to be rebuilt."""
132 |
133 | for item in chain([self.path], self.loader.resolved[self.path]):
134 | if self.loader.modified[item]:
135 | return True
136 |
137 | for args, kwargs in self.loader.assets[self.path]:
138 | kwargs.setdefault('debug', False)
139 | bundle = Bundle(*args, **kwargs)
140 | rv = self.environment.webassets.environment.updater.needs_rebuild(
141 | bundle, self.environment.webassets.environment)
142 | if rv:
143 | return True
144 |
145 | return False
146 |
--------------------------------------------------------------------------------