├── 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 . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | 6 | 7 | class AcrylamidException(Exception): 8 | pass 9 | -------------------------------------------------------------------------------- /acrylamid/filters/hyph/hyph-en-us.chr.txt: -------------------------------------------------------------------------------- 1 | aA 2 | bB 3 | cC 4 | dD 5 | eE 6 | fF 7 | gG 8 | hH 9 | iI 10 | jJ 11 | kK 12 | lL 13 | mM 14 | nN 15 | oO 16 | pP 17 | qQ 18 | rR 19 | sS 20 | tT 21 | uU 22 | vV 23 | wW 24 | xX 25 | yY 26 | zZ 27 | -------------------------------------------------------------------------------- /docs/_themes/werkzeug/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block relbar2 %}{% endblock %} 3 | {%- block footer %} 4 | 8 | {%- endblock %} 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - 3.7 5 | install: 6 | - pip install flake8 7 | before_install: 8 | - python -m pip install --upgrade pip wheel 9 | script: 10 | # - python setup.py test && cram specs/ 11 | - flake8 . --exit-zero 12 | branches: 13 | only: 14 | - master 15 | - patch-1 16 | 17 | -------------------------------------------------------------------------------- /acrylamid/filters/hyph/hyph-fr.chr.txt: -------------------------------------------------------------------------------- 1 | '' 2 | ’’ 3 | aA 4 | bB 5 | cC 6 | dD 7 | eE 8 | fF 9 | gG 10 | hH 11 | iI 12 | jJ 13 | kK 14 | lL 15 | mM 16 | nN 17 | oO 18 | pP 19 | qQ 20 | rR 21 | sS 22 | tT 23 | uU 24 | vV 25 | wW 26 | xX 27 | yY 28 | zZ 29 | àÀ 30 | â 31 | çÇ 32 | èÈ 33 | éÉ 34 | êÊ 35 | îÎ 36 | ïÏ 37 | ôÔ 38 | ûÛ 39 | œŒ 40 | -------------------------------------------------------------------------------- /acrylamid/filters/hyph/hyph-de-1996.chr.txt: -------------------------------------------------------------------------------- 1 | aA 2 | bB 3 | cC 4 | dD 5 | eE 6 | fF 7 | gG 8 | hH 9 | iI 10 | jJ 11 | kK 12 | lL 13 | mM 14 | nN 15 | oO 16 | pP 17 | qQ 18 | rR 19 | sS 20 | tT 21 | uU 22 | vV 23 | wW 24 | xX 25 | yY 26 | zZ 27 | ßß 28 | àÀ 29 | áÁ 30 | â 31 | äÄ 32 | çÇ 33 | èÈ 34 | éÉ 35 | êÊ 36 | ëË 37 | íÍ 38 | ñÑ 39 | óÓ 40 | ôÔ 41 | öÖ 42 | üÜ 43 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | 3 | recursive-exclude acrylamid *.pyc 4 | recursive-include acrylamid/filters/hyph *.txt 5 | 6 | include acrylamid/lib/CHANGES 7 | include acrylamid/views/search/search.js 8 | 9 | include specs/samples/blog.posativ.org.xml 10 | include specs/samples/vlent.nl.xml 11 | include specs/samples/thethreedevelopers.wordpress.2012-04-11.xml 12 | -------------------------------------------------------------------------------- /acrylamid/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2012 Martin Zimmermann . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | 6 | """This module provides a nicer interface to some parts of the standard library, 7 | that are actively used by one or more non-core function. This API may change in 8 | future versions.""" 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Lead developer 2 | 3 | Martin Zimmermann "posativ" 4 | 5 | # Contributers (chronological order) 6 | 7 | - Sebastian Wagner "sebix" 8 | - 0x1cedd1ce 9 | - Moritz Schlarb "moschlar" 10 | - Mark van Lent "markvl" 11 | - the_metalgamer 12 | - Thomas Weißschuh "t-8ch" 13 | - Alexander Zhirov "nevkontakte" 14 | - Christoph Polcin "chripo" 15 | - Daniel Pritchard "dpritchard" 16 | - Brendan Wholihan "hooli" 17 | -------------------------------------------------------------------------------- /acrylamid/filters/rstx_sourcecode.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 docutils.parsers.rst.directives import body 7 | 8 | def register(roles, directives): 9 | for name in 'code-block', 'sourcecode', 'pygments': 10 | directives.register_directive(name, body.CodeBlock) 11 | -------------------------------------------------------------------------------- /acrylamid/filters/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 | import re 7 | from acrylamid.filters import Filter 8 | 9 | 10 | class HTML(Filter): 11 | 12 | match = [re.compile('^(pass|plain|X?HTML)$', re.I)] 13 | version = 1 14 | 15 | conflicts = ['rst', 'md'] 16 | priority = 70.0 17 | 18 | def transform(self, content, entry, *filters): 19 | return content 20 | -------------------------------------------------------------------------------- /docs/_themes/werkzeug/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 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 . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | 6 | from acrylamid.filters import Filter 7 | 8 | try: 9 | from textile import textile 10 | except ImportError: 11 | textile = None # NOQA 12 | 13 | 14 | class PyTextile(Filter): 15 | 16 | match = ['Textile', 'textile', 'pytextile', 'PyTextile'] 17 | version = 1 18 | 19 | conflicts = ['Markdown', 'reStructuredText', 'HTML', 'Pandoc'] 20 | priority = 70.0 21 | 22 | def init(self, conf, env): 23 | 24 | if textile is None: 25 | raise ImportError('Textile: PyTextile not available') 26 | 27 | def transform(self, text, entry, *args): 28 | 29 | return textile(text) 30 | -------------------------------------------------------------------------------- /specs/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import attest 4 | from acrylamid.views import tag 5 | 6 | 7 | class Tag(attest.TestBase): 8 | 9 | @attest.test 10 | def cloud(self): 11 | 12 | tags = {'foo': range(1), 'bar': range(2)} 13 | cloud = tag.Tagcloud(tags, steps=4, max_items=100, start=0) 14 | lst = [(t.name, t.step) for t in cloud] 15 | 16 | assert ('foo', 3) in lst 17 | assert ('bar', 0) in lst 18 | 19 | tags = {'foo': range(1), 'bar': range(2), 'baz': range(4), 'spam': range(8)} 20 | cloud = tag.Tagcloud(tags, steps=4, max_items=4, start=0) 21 | lst = [(t.name, t.step) for t in cloud] 22 | 23 | assert ('foo', 3) in lst 24 | assert ('bar', 2) in lst 25 | assert ('baz', 1) in lst 26 | assert ('spam', 0) in lst 27 | -------------------------------------------------------------------------------- /specs/mako.t: -------------------------------------------------------------------------------- 1 | Test Mako tempating in Acrylamid. 2 | 3 | $ [ -n "$PYTHON" ] || PYTHON="`which python`" 4 | $ LANG="en_US.UTF-8" && unset LC_ALL && unset LANGUAGE 5 | $ acrylamid init -q --mako $TMPDIR 6 | $ cd $TMPDIR 7 | $ acrylamid compile -C 8 | create [?.??s] output/articles/index.html (glob) 9 | create [?.??s] output/2012/die-verwandlung/index.html (glob) 10 | create [0.??s] output/index.html (glob) 11 | create [0.??s] output/tag/die-verwandlung/index.html (glob) 12 | create [0.??s] output/tag/franz-kafka/index.html (glob) 13 | create [?.??s] output/atom/index.html (glob) 14 | create [?.??s] output/rss/index.html (glob) 15 | create [0.??s] output/sitemap.xml (glob) 16 | create output/style.css 17 | 9 new, 0 updated, 0 skipped [?.??s] (glob) 18 | 19 | Clean up: 20 | 21 | $ rm -rf output/ theme/ content/ .cache/ conf.py 22 | -------------------------------------------------------------------------------- /acrylamid/filters/replace.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2014 Christian Koepp . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | 6 | from acrylamid import log 7 | from acrylamid.filters import Filter 8 | 9 | class Replace(Filter): 10 | match = ['replace'] 11 | version = 1 12 | priority = 0.0 13 | 14 | def init(self, conf, env, *args): 15 | try: 16 | self._db = conf.replace_rules 17 | except AttributeError: 18 | log.warn('No configuration named REPLACE_RULES found. Replace filter has nothing to do.') 19 | self._db = dict() 20 | 21 | def transform(self, content, entry, *args): 22 | if len(self._db) == 0: 23 | return content 24 | 25 | for k,v in self._db.items(): 26 | content = content.replace(k, v) 27 | return content 28 | -------------------------------------------------------------------------------- /acrylamid/assets/fallback.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 | from acrylamid.utils import cached_property 8 | 9 | Bundle = lambda *args, **kwargs: None 10 | 11 | 12 | class Webassets(object): 13 | 14 | def __init__(self, conf, env): 15 | pass 16 | 17 | def excludes(self, directory): 18 | return [] 19 | 20 | def compile(self, *args, **kwargs): 21 | return "" 22 | 23 | 24 | class Mixin(object): 25 | 26 | @cached_property 27 | def modified(self): 28 | """Iterate template dependencies for modification.""" 29 | 30 | for item in chain([self.path], self.loader.resolved[self.path]): 31 | if self.loader.modified[item]: 32 | return True 33 | 34 | return False 35 | -------------------------------------------------------------------------------- /docs/api/lib.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Acrylamid Library 3 | ================= 4 | 5 | .. module:: acrylamid.lib 6 | 7 | .. async 8 | 9 | .. automodule:: acrylamid.lib.async 10 | 11 | .. autoclass:: acrylamid.lib.async.Threadpool 12 | :members: 13 | 14 | .. html 15 | 16 | .. automodule:: acrylamid.lib.html 17 | 18 | .. autoclass:: acrylamid.lib.html.HTMLParser 19 | :members: 20 | 21 | .. class:: acrylamid.lib.html.HTMLParseError 22 | 23 | .. httpd 24 | 25 | .. automodule:: acrylamid.lib.httpd 26 | 27 | .. autoclass:: acrylamid.lib.httpd.Webserver 28 | :members: 29 | 30 | .. lazy 31 | 32 | .. automodule:: acrylamid.lib.lazy 33 | 34 | .. autofunction:: acrylamid.lib.lazy.enable 35 | 36 | .. autofunction:: acrylamid.lib.lazy.disable 37 | 38 | .. requests 39 | 40 | .. automodule:: acrylamid.lib.requests 41 | 42 | .. class:: acrylamid.lib.requests.HTTPError 43 | 44 | .. class:: acrylamid.lib.requests.URLError 45 | -------------------------------------------------------------------------------- /acrylamid/filters/python-discount.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 acrylamid.filters import Filter 7 | 8 | try: 9 | from discount import Markdown 10 | except ImportError: 11 | Markdown = None # NOQA 12 | 13 | 14 | class Discount(Filter): 15 | 16 | match = ['discount', 'Discount'] 17 | version = 1 18 | 19 | conflicts = ['Markdown', 'reStructuredText', 'HTML', 'Pandoc', 'typography'] 20 | priority = 70.0 21 | 22 | def init(self, conf, env): 23 | 24 | if Markdown is None: 25 | raise ImportError("Discount: discount not available") 26 | 27 | def transform(self, text, entry, *args): 28 | 29 | mkd = Markdown(text.encode('utf-8'), 30 | autolink=True, safelink=True, ignore_header=True) 31 | return mkd.get_html_content().decode('utf-8') 32 | -------------------------------------------------------------------------------- /acrylamid/filters/head_offset.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 acrylamid.filters import Filter 7 | from re import sub 8 | 9 | 10 | class Headoffset(Filter): 11 | """This filter increases HTML headings by N whereas N is the suffix of 12 | this filter, e.g. `h2' increases headers by two.""" 13 | 14 | version = 1 15 | 16 | def transform(self, text, entry, *args): 17 | 18 | def f(m): 19 | i = int(m.group(1))+1 20 | return ''.join(['', m.group(3), '' % i]) 21 | 22 | for i in range(int(self.name[1])): 23 | text = sub(r']*)>(.+)', f, text) 24 | 25 | return text 26 | 27 | 28 | for offset in range(1, 6): 29 | var = 'h%i' % offset 30 | globals()[var] = type(var, (Headoffset, ), { 31 | 'match': [var], 32 | 'conflicts': ['h%i' % i for i in set([1, 2, 3, 4, 5]) - set([offset])] 33 | }) 34 | -------------------------------------------------------------------------------- /specs/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from attest import AssertImportHook, Tests 4 | AssertImportHook.enable() 5 | 6 | from . import (lib, readers, filters, filters_builtin, helpers, 7 | imprt, views, utils, entry, content, core, search) 8 | 9 | 10 | testsuite = Tests() 11 | testsuite.register(lib.TestHTMLParser) 12 | testsuite.register(readers.tt) 13 | testsuite.register(filters.TestFilterlist) 14 | testsuite.register(filters.TestFilterTree) 15 | testsuite.register(filters_builtin.tt) 16 | testsuite.register(filters_builtin.Hyphenation) 17 | testsuite.register(helpers.Helpers) 18 | testsuite.register(imprt.Import) 19 | testsuite.register(imprt.RSS) 20 | testsuite.register(imprt.Atom) 21 | testsuite.register(imprt.WordPress) 22 | testsuite.register(views.Tag) 23 | testsuite.register(utils.TestMetadata) 24 | testsuite.register(utils.tt) 25 | testsuite.register(entry.TestEntry) 26 | testsuite.register(content.SingleEntry) 27 | testsuite.register(content.MultipleEntries) 28 | testsuite.register(core.Cache) 29 | testsuite.register(search.tt) 30 | -------------------------------------------------------------------------------- /acrylamid/views/index.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 acrylamid.views import View, Paginator 7 | 8 | 9 | class Index(View, Paginator): 10 | """Creates nicely paged listing of your posts. First page renders to ``route`` 11 | (defaults to */*) with a recent list of your (e.g. summarized) articles. Other 12 | pages enumerate to the variable ``pagination`` (*/page/:num/* per default). 13 | 14 | .. code-block:: python 15 | 16 | '/' : { 17 | 'view': 'index', 18 | 'template': 'main.html', 19 | 'pagination': '/page/:num/', 20 | 'items_per_page': 10 21 | } 22 | """ 23 | 24 | export = ['prev', 'curr', 'next', 'items_per_page', 'entrylist'] 25 | template = 'main.html' 26 | 27 | def init(self, *args, **kwargs): 28 | View.init(self, *args, **kwargs) 29 | Paginator.init(self, *args, **kwargs) 30 | self.filters.append('relative') 31 | -------------------------------------------------------------------------------- /acrylamid/filters/hyph/hyph-de-1996.lic.txt: -------------------------------------------------------------------------------- 1 | German Hyphenation Patterns (Reformed Orthography) 2 | 3 | (more info about the licence to be added later) 4 | 5 | % dehyphn-x-2011-06-01.pat 6 | 7 | \message{German Hyphenation Patterns (Reformed Orthography, 2006) `dehyphn-x' 2011-06-01 (WL)} 8 | 9 | % TeX-Trennmuster für die reformierte (2006) deutsche Rechtschreibung 10 | % 11 | % 12 | % Copyright (C) 2007, 2008, 2009, 2011 Werner Lemberg 13 | % 14 | % This program can be redistributed and/or modified under the terms 15 | % of the LaTeX Project Public License Distributed from CTAN 16 | % archives in directory macros/latex/base/lppl.txt; either 17 | % version 1 of the License, or any later version. 18 | % 19 | % 20 | % The word list is available from 21 | % 22 | % http://repo.or.cz/w/wortliste.git?a=commit;h=2d246449f5c4f570f4d735d3ad091f6ad70f6972 23 | % 24 | % The used patgen parameters are 25 | % 26 | % 1 1 | 2 5 | 1 1 1 27 | % 2 2 | 2 5 | 1 2 1 28 | % 3 3 | 2 6 | 1 1 1 29 | % 4 4 | 2 6 | 1 4 1 30 | % 5 5 | 2 7 | 1 1 1 31 | % 6 6 | 2 7 | 1 6 1 32 | % 7 7 | 2 13 | 1 4 1 33 | % 8 8 | 2 13 | 1 8 1 34 | 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Attest-latest==0.6.1.dev20130603 2 | Babel==2.6.0 3 | bleach==3.1.0 4 | blockdiag==1.5.4 5 | certifi==2019.3.9 6 | chardet==3.0.4 7 | colorama==0.4.1 8 | cram==0.7 9 | docutils==0.14 10 | filelock==3.0.10 11 | funcparserlib==0.3.6 12 | idna==2.8 13 | imagesize==1.1.0 14 | Jinja2==2.10.1 15 | Mako==1.0.9 16 | Markdown==3.1 17 | MarkupSafe==1.1.1 18 | packaging==19.0 19 | Pillow==6.0.0 20 | pkginfo==1.5.0.1 21 | pluggy==0.9.0 22 | progressbar-latest==2.4 23 | py==1.8.0 24 | Pygments==2.3.1 25 | pyparsing==2.4.0 26 | pytz==2019.1 27 | readme-renderer==24.0 28 | requests==2.21.0 29 | requests-toolbelt==0.9.1 30 | six==1.12.0 31 | smartypants==2.0.1 32 | snowballstemmer==1.2.1 33 | Sphinx==2.0.1 34 | sphinxcontrib-applehelp==1.0.1 35 | sphinxcontrib-blockdiag==1.5.5 36 | sphinxcontrib-devhelp==1.0.1 37 | sphinxcontrib-htmlhelp==1.0.2 38 | sphinxcontrib-jsmath==1.0.1 39 | sphinxcontrib-qthelp==1.0.2 40 | sphinxcontrib-serializinghtml==1.1.3 41 | toml==0.10.0 42 | tox==3.9.0 43 | tqdm==4.31.1 44 | translitcodec==0.4.0 45 | twine==1.13.0 46 | urllib3==1.24.2 47 | virtualenv==16.5.0 48 | webcolors==1.8.1 49 | webencodings==0.5.1 50 | zest.releaser==6.18.2 51 | -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /acrylamid/filters/rstx_highlight.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 docutils import nodes 7 | from docutils.parsers.rst import Directive 8 | from xml.sax.saxutils import escape 9 | 10 | 11 | class Highlight(Directive): 12 | """Wrap source code to be used with `Highlight.js`_: 13 | 14 | .. _highlight.js: http://softwaremaniacs.org/soft/highlight/en/ 15 | 16 | .. code-block:: rst 17 | 18 | .. highlight-js:: python 19 | 20 | print("Hello, World!") 21 | """ 22 | 23 | optional_arguments = 1 24 | has_content = True 25 | 26 | def run(self): 27 | lang = None 28 | if len(self.arguments) >= 1: 29 | lang = self.arguments[0] 30 | if lang: 31 | tmpl = '
%%s
' % 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 . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | 6 | """ 7 | Requests 8 | ~~~~~~~~ 9 | 10 | A simple wrapper around urllib2. 11 | 12 | .. function:: head(url, **hdrs) 13 | 14 | Sends a HEAD request to given url but does not catch any exception. 15 | 16 | :param url: url to send the request to 17 | :param hdrs: a key-value pair that is send within the HTTP header 18 | 19 | .. function:: get(url, **hdrs) 20 | 21 | Same like :func:`head` but for GET.""" 22 | 23 | try: 24 | from urllib2 import Request, urlopen, HTTPError, URLError 25 | except ImportError: 26 | from urllib.request import Request, urlopen 27 | from urllib.error import HTTPError, URLError 28 | 29 | 30 | def proto(method, url, **hdrs): 31 | 32 | headers = {'User-Agent': "Mozilla/5.0 Gecko/20120427 Firefox/15.0"} 33 | headers.update(hdrs) 34 | 35 | req = Request(url, headers=headers) 36 | req.get_method = lambda : method 37 | 38 | return urlopen(req, timeout=10) 39 | 40 | 41 | head = lambda url, **hdrs: proto('HEAD', url, **hdrs) 42 | get = lambda url, **hdrs: proto('GET', url, **hdrs) 43 | 44 | 45 | __all__ = ['head', 'get', 'HTTPError', 'URLError'] 46 | -------------------------------------------------------------------------------- /misc/search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import unicode_literals 4 | 5 | import sys 6 | import os 7 | import io 8 | import json 9 | 10 | 11 | def find(node): 12 | 13 | if len(node) == 2: 14 | yield node[1] 15 | 16 | for key in node[0]: 17 | find(node[0][key]) 18 | 19 | 20 | def search(needle, haystack): 21 | 22 | if needle[0] not in haystack: 23 | return False 24 | 25 | node = haystack[needle[0]] 26 | needle = needle[1:] 27 | i, j = 0, 0 28 | 29 | while j < len(needle): 30 | 31 | if needle[i:j+1] in node[0]: 32 | node = node[0][needle[i:j+1]] 33 | i = j + 1 34 | 35 | j += 1 36 | 37 | if i != j: 38 | return False 39 | 40 | if len(node) == 2: 41 | print 'exact match:', node[1] 42 | 43 | rest = [] 44 | for key in node[0]: 45 | rest.append(list(find(node[0][key]))) 46 | print 'partial match:', sum(sum(rest, []), []) 47 | 48 | 49 | if __name__ == '__main__': 50 | 51 | if len(sys.argv) < 3: 52 | print 'usage: %s /path/to/[a-z].js keyword' % sys.argv[0] 53 | sys.exit(1) 54 | 55 | with io.open(sys.argv[1]) as fp: 56 | tree = {os.path.basename(sys.argv[1])[0]: json.load(fp)} 57 | 58 | search(sys.argv[2].decode('utf-8'), tree) 59 | -------------------------------------------------------------------------------- /acrylamid/filters/pandoc.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 acrylamid.filters import Filter 7 | from acrylamid.helpers import system 8 | from acrylamid.errors import AcrylamidException 9 | 10 | 11 | class Pandoc(Filter): 12 | 13 | match = ['Pandoc', 'pandoc'] 14 | version = 1 15 | 16 | conflicts = ['Markdown', 'reStructuredText', 'HTML'] 17 | priority = 70.0 18 | 19 | def init(self, conf, env): 20 | self.ignore = env.options.ignore 21 | 22 | def transform(self, text, entry, *args): 23 | 24 | try: 25 | system(['which', 'pandoc']) 26 | except AcrylamidException: 27 | if self.ignore: 28 | return text 29 | raise AcrylamidException('Pandoc: pandoc not available') 30 | 31 | if len(args) == 0: 32 | raise AcrylamidException("pandoc filter takes one or more arguments") 33 | 34 | fmt, extras = args[0], args[1:] 35 | cmd = ['pandoc', '-f', fmt, '-t', 'HTML'] 36 | cmd.extend(['--'+x for x in extras]) 37 | 38 | try: 39 | return system(cmd, stdin=text) 40 | except OSError as e: 41 | raise AcrylamidException(e.msg) 42 | -------------------------------------------------------------------------------- /specs/lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from attest import test, TestBase 4 | 5 | from acrylamid.lib.html import HTMLParser 6 | 7 | f = lambda x: ''.join(HTMLParser(x).result) 8 | 9 | 10 | class TestHTMLParser(TestBase): 11 | 12 | @test 13 | def starttag(self): 14 | 15 | examples = [ 16 | '

', 17 | '

', 18 | '', 19 | '', 20 | ] 21 | 22 | for ex in examples: 23 | assert f(ex) == ex 24 | 25 | @test 26 | def data(self): 27 | assert f('

Data!1

') == '

Data!1

' 28 | 29 | @test 30 | def endtag(self): 31 | 32 | examples = [ 33 | '

', 34 | '

'*3, 35 | ] 36 | 37 | for ex in examples: 38 | assert f(ex) == ex 39 | 40 | @test 41 | def startendtag(self): 42 | 43 | for ex in ['
', '']: 44 | assert f(ex) == ex 45 | 46 | @test 47 | def entityrefs(self): 48 | 49 | assert f('&') == '&' 50 | assert f('&foo;') == '&foo;' 51 | 52 | @test 53 | def charrefs(self): 54 | 55 | assert f('Ӓ') == 'Ӓ' 56 | -------------------------------------------------------------------------------- /specs/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import attest 4 | from acrylamid.core import cache 5 | 6 | 7 | class Cache(attest.TestBase): 8 | 9 | def __context__(self): 10 | with attest.tempdir() as path: 11 | self.path = path 12 | cache.init(self.path) 13 | 14 | yield 15 | 16 | @attest.test 17 | def persistence(self): 18 | 19 | cache.init(self.path) 20 | cache.set('foo', 'bar', "Hello World!") 21 | cache.set('foo', 'baz', "spam") 22 | assert cache.get('foo', 'bar') == "Hello World!" 23 | assert cache.get('foo', 'baz') == "spam" 24 | 25 | cache.shutdown() 26 | cache.init(self.path) 27 | assert cache.get('foo', 'bar') == "Hello World!" 28 | assert cache.get('foo', 'baz') == "spam" 29 | 30 | @attest.test 31 | def remove(self): 32 | 33 | cache.init(self.path) 34 | cache.set('foo', 'bar', 'baz') 35 | cache.remove('foo') 36 | cache.remove('invalid') 37 | 38 | assert cache.get('foo', 'bar') == None 39 | assert cache.get('invalid', 'bla') == None 40 | 41 | @attest.test 42 | def clear(self): 43 | 44 | cache.init(self.path) 45 | cache.set('foo', 'bar', 'baz') 46 | cache.set('spam', 'bar', 'baz') 47 | 48 | cache.clear() 49 | assert cache.get('foo', 'bar') == None 50 | assert cache.get('spam', 'bar') == None 51 | -------------------------------------------------------------------------------- /specs/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from acrylamid.utils import Metadata, neighborhood 4 | 5 | import attest 6 | tt = attest.Tests() 7 | 8 | 9 | class TestMetadata(attest.TestBase): 10 | 11 | @attest.test 12 | def works(self): 13 | 14 | dct = Metadata() 15 | dct['hello.world'] = 1 16 | 17 | assert dct['hello']['world'] == 1 18 | assert dct.hello.world == 1 19 | 20 | try: 21 | dct.foo 22 | dct.foo.bar 23 | except KeyError: 24 | assert True 25 | else: 26 | assert False 27 | 28 | dct['hello.foreigner'] = 2 29 | 30 | assert dct['hello']['world'] == 1 31 | assert dct.hello.world == 1 32 | 33 | assert dct.hello.foreigner == 2 34 | 35 | @attest.test 36 | def redirects(self): 37 | 38 | dct = Metadata() 39 | alist = [1, 2, 3] 40 | 41 | dct['foo'] = alist 42 | dct.redirect('foo', 'baz') 43 | 44 | assert 'foo' not in dct 45 | assert 'baz' in dct 46 | assert dct['baz'] == alist 47 | 48 | 49 | @attest.test 50 | def update(self): 51 | 52 | dct = Metadata() 53 | dct.update({'hello.world': 1}) 54 | 55 | assert 'hello' in dct 56 | assert dct.hello.world == 1 57 | 58 | @attest.test 59 | def init(self): 60 | assert Metadata({'hello.world': 1}).hello.world == 1 61 | 62 | 63 | @tt.test 64 | def neighbors(): 65 | 66 | assert list(neighborhood([1, 2, 3])) == \ 67 | [(None, 1, 2), (1, 2, 3), (2, 3, None)] 68 | -------------------------------------------------------------------------------- /acrylamid/compat.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2013 Armin Ronacher . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | # 6 | # http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ 7 | 8 | import sys 9 | PY2K = sys.version_info[0] == 2 10 | 11 | if not PY2K: 12 | 13 | unichr = chr 14 | text_type = str 15 | string_types = (str, ) 16 | implements_to_string = lambda x: x 17 | 18 | map, zip, filter = map, zip, filter 19 | 20 | iterkeys = lambda d: iter(d.keys()) 21 | itervalues = lambda d: iter(d.values()) 22 | iteritems = lambda d: iter(d.items()) 23 | 24 | else: 25 | 26 | unichr = unichr 27 | text_type = unicode 28 | string_types = (str, unicode) 29 | 30 | from itertools import imap, izip, ifilter 31 | map, zip, filter = imap, izip, ifilter 32 | 33 | def implements_to_string(cls): 34 | 35 | cls.__unicode__ = cls.__str__ 36 | cls.__str__ = lambda x: x.__unicode__().encode('utf-8') 37 | return cls 38 | 39 | iterkeys = lambda d: d.iterkeys() 40 | itervalues = lambda d: d.itervalues() 41 | iteritems = lambda d: d.iteritems() 42 | 43 | 44 | def metaclass(meta, *bases): 45 | 46 | class Meta(meta): 47 | 48 | __call__ = type.__call__ 49 | __init__ = type.__init__ 50 | 51 | def __new__(cls, name, this_bases, d): 52 | if this_bases is None: 53 | return type.__new__(cls, name, (), d) 54 | return meta(name, bases, d) 55 | 56 | return Meta('temporary_class', None, {}) 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Martin Zimmermann and Contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | The views and conclusions contained in the software and documentation are 26 | those of the authors and should not be interpreted as representing official 27 | policies, either expressed or implied, of Martin Zimmermann . -------------------------------------------------------------------------------- /acrylamid/colors.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 sys 7 | 8 | from acrylamid import compat 9 | from acrylamid.compat import text_type as str, string_types 10 | 11 | if sys.platform == 'win32': 12 | import colorama 13 | colorama.init() 14 | 15 | 16 | @compat.implements_to_string 17 | class ANSIString(object): 18 | 19 | style = 0 20 | color = 30 21 | 22 | def __init__(self, obj, style=None, color=None): 23 | 24 | if isinstance(obj, ANSIString): 25 | if style is None: 26 | style = obj.style 27 | if color is None: 28 | color = obj.color 29 | obj = obj.obj 30 | elif not isinstance(obj, string_types): 31 | obj = str(obj) 32 | 33 | self.obj = obj 34 | if style: 35 | self.style = style 36 | if color: 37 | self.color = color 38 | 39 | def __str__(self): 40 | return '\033[%i;%im' % (self.style, self.color) + self.obj + '\033[0m' 41 | 42 | def __add__(self, other): 43 | return str.__add__(str(self), other) 44 | 45 | def __radd__(self, other): 46 | return other + str(self) 47 | 48 | def encode(self, encoding): 49 | return str(self).encode(encoding) 50 | 51 | 52 | normal, bold, underline = [lambda obj, x=x: ANSIString(obj, style=x) 53 | for x in (0, 1, 4)] 54 | 55 | black, red, green, yellow, blue, \ 56 | magenta, cyan, white = [lambda obj, y=y: ANSIString(obj, color=y) 57 | for y in range(30, 38)] 58 | -------------------------------------------------------------------------------- /specs/init.t: -------------------------------------------------------------------------------- 1 | Testing `acrylamid init` in different ways: 2 | 3 | $ [ -n "$PYTHON" ] || PYTHON="`which python`" 4 | $ LANG="en_US.UTF-8" && unset LC_ALL && unset LANGUAGE 5 | $ cd $TMPDIR 6 | 7 | Setup in current directory? 8 | 9 | $ acrylamid init -C . 10 | create ./content/sample-entry.txt 11 | create ./theme/base.html 12 | create ./theme/main.html 13 | create ./theme/entry.html 14 | create ./theme/articles.html 15 | create ./theme/atom.xml 16 | create ./theme/rss.xml 17 | create ./theme/style.css 18 | create ./conf.py 19 | Created your fresh new blog at '.'. Enjoy! 20 | 21 | Now set up in a given directory: 22 | 23 | $ rm -rf ./theme ./content conf.py 24 | $ cd ../ 25 | $ acrylamid init -C foo 26 | create foo/content/sample-entry.txt 27 | create foo/theme/base.html 28 | create foo/theme/main.html 29 | create foo/theme/entry.html 30 | create foo/theme/articles.html 31 | create foo/theme/atom.xml 32 | create foo/theme/rss.xml 33 | create foo/theme/style.css 34 | create foo/conf.py 35 | Created your fresh new blog at 'foo'. Enjoy! 36 | 37 | Can we find all needed files? 38 | 39 | $ cd foo/ 40 | $ [ -e conf.py ] 41 | $ [ -e content/sample-entry.txt ] 42 | 43 | $ [ -e theme/base.html ] 44 | $ [ -e theme/main.html ] 45 | $ [ -e theme/entry.html ] 46 | $ [ -e theme/articles.html ] 47 | $ [ -e theme/rss.xml ] 48 | $ [ -e theme/atom.xml ] 49 | $ [ -e theme/style.css ] 50 | 51 | Can we restore our stylesheet? 52 | 53 | $ rm theme/style.css 54 | $ acrylamid init -C theme/style.css 55 | re-initialized theme/style.css 56 | 57 | And we should clean up everything: 58 | 59 | $ rm -rf output/ theme/ content/ .cache/ conf.py 60 | -------------------------------------------------------------------------------- /acrylamid/filters/strip.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 acrylamid import log 7 | 8 | from acrylamid.filters import Filter 9 | from acrylamid.lib.html import HTMLParser 10 | 11 | 12 | class Text(HTMLParser): 13 | """Strip tags and attributes from HTML. By default it keeps everything 14 | between any HTML tags, but you can supply a list of ignored tags.""" 15 | 16 | handle_comment = handle_startendtag = lambda *x, **z: None 17 | 18 | def __init__(self, html, args): 19 | 20 | self.ignored = args 21 | super(Text, self).__init__(html) 22 | 23 | def handle_starttag(self, tag, attrs): 24 | self.stack.append(tag) 25 | 26 | def handle_endtag(self, tag): 27 | try: 28 | self.stack.pop() 29 | except IndexError: 30 | pass 31 | 32 | if tag in ('li', 'ul', 'p'): 33 | self.result.append('\n') 34 | 35 | def handle_data(self, data): 36 | if not any(tag for tag in self.ignored if tag in self.stack): 37 | super(Text, self).handle_data(data) 38 | 39 | def handle_entityref(self, name): 40 | if name == 'shy': 41 | return 42 | self.handle_data(self.unescape('&' + name + ';')) 43 | 44 | def handle_charref(self, char): 45 | self.handle_data(self.unescape('&#' + char + ';')) 46 | 47 | 48 | class Strip(Filter): 49 | 50 | match = ['strip'] 51 | version = 1 52 | priority = 0.0 53 | 54 | def transform(self, content, entry, *args): 55 | 56 | try: 57 | return ''.join(Text(content, args).result) 58 | except: 59 | log.exception('could not strip ' + entry.filename) 60 | return content 61 | -------------------------------------------------------------------------------- /acrylamid/filters/metalogo.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2012 sebix . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | # Idea by http://nitens.org/taraborelli/texlogo 6 | 7 | from acrylamid.filters import Filter 8 | 9 | LaTeX = """\ 10 | L 11 | aT 12 | e 13 | X 14 | """.strip().replace('\n', '') 15 | 16 | TeX = """\ 17 | T 18 | e 19 | X 20 | """.strip().replace('\n', '') 21 | 22 | XeTeX = u"""\ 23 | X 24 | ǝ 25 | T 26 | e 27 | X 28 | """.strip().replace('\n', '') 29 | 30 | 31 | class Tex(Filter): 32 | 33 | match = ['metalogo'] 34 | version = 3 35 | 36 | def transform(self, text, entry, *args): 37 | replacings = (('LaTeX', LaTeX), 38 | ('XeTeX', XeTeX), 39 | ('TeX', TeX)) 40 | for k in replacings: 41 | text = text.replace(k[0], k[1]) 42 | return text 43 | -------------------------------------------------------------------------------- /acrylamid/templates/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2012 Martin Zimmermann . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | # 6 | # Provide a homogenous interface to Templating Engines like Jinja2 7 | 8 | import abc 9 | 10 | 11 | class AbstractEnvironment(object): 12 | """Generic interface for python templating engines like Jinja2 or Mako.""" 13 | 14 | __metaclass__ = abc.ABCMeta 15 | 16 | extension = ['.html'] 17 | 18 | @abc.abstractmethod 19 | def __init__(self, layoutdir, cachedir): 20 | """Initialize templating engine and set default layoutdir as well 21 | as cache dir. You should use a custom cache filename prefix like 22 | *__engine_hexcode.cache*.""" 23 | return 24 | 25 | @abc.abstractmethod 26 | def register(self, name, func): 27 | """Register a :param function: to :param name:""" 28 | return 29 | 30 | @abc.abstractmethod 31 | def fromfile(self, env, path): 32 | """Load (relative) :param path: template and return a 33 | :class:`AbstractTemplate`-like class`.""" 34 | return 35 | 36 | @abc.abstractmethod 37 | def extend(self, path): 38 | """Extend search PATH for templates by `path`.""" 39 | return 40 | 41 | @abc.abstractproperty 42 | def loader(self): 43 | return 44 | 45 | 46 | class AbstractTemplate(object): 47 | 48 | __metaclass__ = abc.ABCMeta 49 | 50 | def __init__(self, environment, path, template): 51 | self.environment = environment 52 | 53 | self.path = path 54 | self.template = template 55 | 56 | self.engine = environment.engine 57 | self.loader = environment.engine.loader 58 | 59 | @abc.abstractmethod 60 | def render(self, **dikt): 61 | """Render template with :param dikt:""" 62 | return 63 | -------------------------------------------------------------------------------- /acrylamid/filters/mako-templating.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2012 moschlar . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | 6 | from acrylamid import log 7 | from acrylamid.filters import Filter 8 | from acrylamid.helpers import system as defaultsystem 9 | from acrylamid.errors import AcrylamidException 10 | from acrylamid.compat import text_type as str 11 | 12 | try: 13 | from mako.template import Template 14 | from mako.exceptions import MakoException 15 | except ImportError: 16 | Template = None # NOQA 17 | MakoException = None # NOQA 18 | 19 | 20 | class Mako(Filter): 21 | """Mako filter that pre-processes in Markdown/reStructuredText 22 | written posts. XXX: and offers some Mako extensions.""" 23 | 24 | match = ['Mako', 'mako'] 25 | version = 1 26 | 27 | priority = 90.0 28 | 29 | def init(self, conf, env, *args): 30 | 31 | if not Mako or not MakoException: 32 | raise ImportError('Mako: No module named mako') 33 | 34 | def system(cmd, stdin=None): 35 | try: 36 | return defaultsystem(cmd, stdin, shell=True).strip() 37 | except (OSError, AcrylamidException) as e: 38 | log.warn('%s: %s' % (e.__class__.__name__, e.args[0])) 39 | return e.args[0] 40 | 41 | self.conf = conf 42 | self.env = env 43 | self.filters = {'system': system, 'split': str.split} 44 | 45 | def transform(self, content, entry): 46 | 47 | try: 48 | tt = Template(content, cache_enabled=False, input_encoding='utf-8') 49 | return tt.render(conf=self.conf, env=self.env, entry=entry, **self.filters) 50 | except (MakoException, AcrylamidException) as e: 51 | log.warn('%s: %s in %r' % (e.__class__.__name__, e.args[0], entry.filename)) 52 | return content 53 | -------------------------------------------------------------------------------- /docs/_themes/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 by Armin Ronacher. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms of the theme, with or 6 | without modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | We kindly ask you to only use these themes in an unmodified manner just 22 | for Flask and Flask-related products, not for unrelated projects. If you 23 | like the visual style and want to use it for your own projects, please 24 | consider making some larger changes to the themes (such as changing 25 | font faces, sizes, colors or margins). 26 | 27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE 37 | POSSIBILITY OF SUCH DAMAGE. 38 | -------------------------------------------------------------------------------- /acrylamid/filters/hyph/hyph-en-us.lic.txt: -------------------------------------------------------------------------------- 1 | Hyphenation Patterns for American English 2 | 3 | (more info about the licence to be added later) 4 | 5 | % This file has been renamed from ushyphmax.tex to hyph-en-us.tex in June 2008 6 | % for consistency with other files with hyphenation patterns in hyph-utf8 package. 7 | % No other changes made. See http://www.tug.org/tex-hyphen for more details. 8 | 9 | % ushyphmax.tex -- patterns for more hyphenation pattern memory (12000+). 10 | % Also known as ushyphen.max. 11 | % 12 | % version of 2005-05-30. 13 | % Patterns of March 1, 1990. 14 | % 15 | % Copyright (C) 1990, 2004, 2005 Gerard D.C. Kuiken. 16 | % Copying and distribution of this file, with or without modification, 17 | % are permitted in any medium without royalty provided the copyright 18 | % notice and this notice are preserved. 19 | % 20 | % Needs extended pattern memory. 21 | % Hyphenation trie becomes 7283 with 377 ops. 22 | % 23 | % These patterns are based on the Hyphenation Exception Log 24 | % published in TUGboat, Volume 10 (1989), No. 3, pp. 337-341, 25 | % and a large number of incorrectly hyphenated words not yet published. 26 | % If added to Liang's before the closing bracket } of \patterns, 27 | % the patterns run errorfree as far as known at this moment. 28 | % 29 | % These patterns find all admissible hyphens of the words in 30 | % the Exception Log. ushyph2.tex is a smaller set. 31 | % 32 | % Please send bugs or suggestions to tex-live (at) tug.org. 33 | % 34 | % 2005-05-30 (karl): in the past, ushyphmax.tex was a file containing 35 | % only the additional patterns, without the \patterns command, etc. 36 | % This turned out not to be very useful, since in practice the TeX 37 | % distributions need one self-contained file for a language. Therefore, 38 | % ushyphmax.tex now contains both the additional patterns from 39 | % Dr. Kuiken, and the original patterns and hyphenations from Knuth's 40 | % hyphen.tex. 41 | % 42 | % The Plain TeX hyphenation tables. 43 | -------------------------------------------------------------------------------- /acrylamid/filters/mdx_subscript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2010, Shane Graber 4 | # 5 | # Subscript extension for Markdown. 6 | # 7 | # To subscript something, place a tilde symbol, '~', before and after the 8 | # text that you would like in subscript: C~6~H~12~O~6~ 9 | # The numbers in this example will be subscripted. See below for more: 10 | # 11 | # Examples: 12 | # 13 | # >>> import markdown 14 | # >>> md = markdown.Markdown(extensions=['subscript']) 15 | # >>> md.convert('This is sugar: C~6~H~12~O~6~') 16 | # u'

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, see . 17 | 18 | import re 19 | import markdown 20 | import asciimathml 21 | 22 | match = ['mathml', 'math', 'asciimathml', 'MathML', 'Math', 'AsciiMathML'] 23 | __author__ = 'Gabriele Favalessa' 24 | 25 | RE = re.compile(r'^(.*)\$([^\$]*)\$(.*)$', re.M) # $ a $ 26 | 27 | 28 | class ASCIIMathMLExtension(markdown.Extension): 29 | def __init__(self, configs): 30 | pass 31 | 32 | def extendMarkdown(self, md, md_globals): 33 | self.md = md 34 | md.inlinePatterns.add('', ASCIIMathMLPattern(RE), '_begin') 35 | 36 | def reset(self): 37 | pass 38 | 39 | 40 | class ASCIIMathMLPattern(markdown.inlinepatterns.Pattern): 41 | def getCompiledRegExp(self): 42 | return RE 43 | 44 | def handleMatch(self, m): 45 | if markdown.version_info < (2, 1, 0): 46 | math = asciimathml.parse(m.group(2).strip(), markdown.etree.Element, 47 | markdown.AtomicString) 48 | else: 49 | math = asciimathml.parse(m.group(2).strip(), 50 | markdown.util.etree.Element, markdown.util.AtomicString) 51 | math.set('xmlns', 'http://www.w3.org/1998/Math/MathML') 52 | return math 53 | 54 | 55 | def makeExtension(configs=None): 56 | return ASCIIMathMLExtension(configs=configs) 57 | -------------------------------------------------------------------------------- /misc/bash_completion: -------------------------------------------------------------------------------- 1 | _acrylamid() { 2 | local cur prev opts 3 | COMPREPLY=() 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | opts="init compile view autocompile clean import deploy dp new check 7 | info ping --help --version --no-color --verbose --quiet" 8 | 9 | case "${prev}" in 10 | init) 11 | COMPREPLY=( $(compgen -W "--force --mako --jinja2 --theme" -- ${cur}) ) 12 | return 0 13 | ;; 14 | new) 15 | COMPREPLY=( $(compgen -W "" -- ${cur}) ) 16 | return 0 17 | ;; 18 | compile|co|gen|generate) 19 | COMPREPLY=( $(compgen -W "--force --dry-run --ignore" -- ${cur}) ) 20 | return 0 21 | ;; 22 | view) 23 | COMPREPLY=( $(compgen -W "--port" -- ${cur}) ) 24 | return 0 25 | ;; 26 | autocompile|aco) 27 | COMPREPLY=( $(compgen -W "--force --dry-run --ignore --port" -- ${cur}) ) 28 | return 0 29 | ;; 30 | clean|rm) 31 | COMPREPLY=( $(compgen -W "--force --dry-run" -- ${cur}) ) 32 | return 0 33 | ;; 34 | import) 35 | COMPREPLY=( $(compgen -W "--force --keep-links --pandoc" -- ${cur}) ) 36 | return 0 37 | ;; 38 | deploy|dp) 39 | local keys=$(for x in `acrylamid deploy`; do echo ${x}; 40 | done) 41 | COMPREPLY=( $(compgen -W "${keys}" -- ${cur}) ) 42 | return 0 43 | ;; 44 | check) 45 | COMPREPLY=( $(compgen -W "W3C links" -- ${cur}) ) 46 | return 0 47 | ;; 48 | info) 49 | COMPREPLY=( $(compgen -W "" -- ${cur}) ) 50 | return 0 51 | ;; 52 | ping) 53 | COMPREPLY=( $(compgen -W "back twitter" -- ${cur}) ) 54 | return 0 55 | ;; 56 | esac 57 | 58 | COMPREPLY=($(compgen -W "${opts}" -- ${cur})) 59 | } 60 | complete -o default -F _acrylamid acrylamid 61 | -------------------------------------------------------------------------------- /acrylamid/views/articles.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 acrylamid.views import View 7 | from acrylamid.helpers import union, joinurl, event 8 | 9 | from os.path import exists 10 | 11 | 12 | class Articles(View): 13 | """Generates an overview of all articles using *layouts/articles.html* as 14 | default jinja2 template (`Example `_). 15 | 16 | To enable Articles view, add: 17 | 18 | .. code-block:: python 19 | 20 | '/articles/' : { 21 | 'view': 'articles', 22 | 'template': 'articles.html' # default 23 | } 24 | 25 | to your :doc:`conf.py` where */articles/* is the default URL for this view. 26 | 27 | We filter articles that are drafts and add them to the *articles* 28 | dictionary using ``(entry.year, entry.imonth)`` as key. During templating 29 | we sort all keys by value, hence we get a listing of years > months > entries. 30 | 31 | Variables available during Templating: 32 | 33 | - *articles* containing the articles 34 | - *num_entries* count of articles 35 | - *conf*, *env*""" 36 | 37 | priority = 80.0 38 | 39 | def init(self, conf, env, template='articles.html'): 40 | self.template = template 41 | 42 | def generate(self, conf, env, data): 43 | 44 | entrylist = data['entrylist'] 45 | 46 | tt = env.engine.fromfile(env, self.template) 47 | path = joinurl(conf['output_dir'], self.path, 'index.html') 48 | 49 | if exists(path) and not (conf.modified or env.modified or tt.modified): 50 | event.skip('article', path) 51 | raise StopIteration 52 | 53 | articles = {} 54 | for entry in entrylist: 55 | articles.setdefault((entry.year, entry.imonth), []).append(entry) 56 | 57 | html = tt.render(conf=conf, articles=articles, env=union(env, 58 | num_entries=len(entrylist), route=self.path)) 59 | yield html, path 60 | -------------------------------------------------------------------------------- /acrylamid/lib/_async.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2012 Martin Zimmermann . All rights reserved. 4 | # via http://code.activestate.com/recipes/577187-python-thread-pool/ 5 | 6 | """ 7 | Asynchronous Tasks 8 | ~~~~~~~~~~~~~~~~~~ 9 | 10 | A simple thread pool implementation, that can be used for parallel I/O. 11 | 12 | Example usage:: 13 | 14 | >>> def takes(long=10): 15 | ... sleep(long) 16 | ... 17 | >>> pool = Threadpool(5) 18 | >>> for x in range(10): 19 | ... pool.add_task(takes, x) 20 | >>> pool.wait_completion() 21 | 22 | You can't retrieve the return values, just wait until they finish.""" 23 | 24 | from threading import Thread 25 | from acrylamid import log 26 | from acrylamid.compat import PY2K, text_type as str 27 | 28 | if PY2K: 29 | from Queue import Queue 30 | else: 31 | from queue import Queue 32 | 33 | 34 | class Worker(Thread): 35 | """Thread executing tasks from a given tasks queue""" 36 | 37 | def __init__(self, tasks): 38 | Thread.__init__(self) 39 | self.tasks = tasks 40 | self.daemon = True 41 | self.start() 42 | 43 | def run(self): 44 | while True: 45 | func, args, kargs = self.tasks.get() 46 | try: 47 | func(*args, **kargs) 48 | except Exception as e: 49 | log.exception('%s: %s' % (e.__class__.__name__, str(e))) 50 | self.tasks.task_done() 51 | 52 | 53 | class Threadpool: 54 | """Initialize pool with number of workers, that run a function with 55 | given arguments and catch all exceptions.""" 56 | 57 | def __init__(self, num_threads, wait=True): 58 | self.tasks = Queue(num_threads if wait else 0) 59 | self.wait = wait 60 | for _ in range(num_threads): 61 | Worker(self.tasks) 62 | 63 | def add_task(self, func, *args, **kargs): 64 | """Add a task to the queue""" 65 | self.tasks.put((func, args, kargs), self.wait) 66 | 67 | def wait_completion(self): 68 | """Wait for completion of all the tasks in the queue""" 69 | self.tasks.join() 70 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | import pathlib 6 | from setuptools import setup, find_packages 7 | 8 | requires = ['Jinja2>=2.4', 'Markdown>=2.0.1', 'unidecode>=0.04.13'] 9 | 10 | if sys.version_info < (2, 7): 11 | requires += ['argparse', 'ordereddict'] 12 | 13 | if sys.platform == 'win32': 14 | requires.append('colorama') 15 | 16 | # The directory containing this file 17 | HERE = pathlib.Path(__file__).parent 18 | 19 | # The text of the README file 20 | README = (HERE / 'README.md').read_text(encoding="utf-8") 21 | 22 | setup( 23 | name='acrylamid', 24 | version='0.8.dev0', 25 | author='Martin Zimmermann', 26 | author_email='info@posativ.org', 27 | packages=find_packages(), 28 | include_package_data=True, 29 | zip_safe=False, 30 | url='http://posativ.org/acrylamid/', 31 | license='BSD revised', 32 | description='static blog compiler with incremental updates', 33 | long_description=README, 34 | long_description_content_type="text/markdown", 35 | classifiers=[ 36 | "Development Status :: 4 - Beta", 37 | "Topic :: Internet", 38 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary", 39 | "Environment :: Console", 40 | "Intended Audience :: End Users/Desktop", 41 | "License :: OSI Approved :: BSD License", 42 | "Programming Language :: Python", 43 | "Programming Language :: Python :: 2", 44 | "Programming Language :: Python :: 2.6", 45 | "Programming Language :: Python :: 2.7", 46 | "Programming Language :: Python :: 3", 47 | "Programming Language :: Python :: 3.3", 48 | ], 49 | install_requires=requires, 50 | extras_require={ 51 | 'full': ['pygments', 'docutils>=0.9', 'smartypants', 'asciimathml', 52 | 'textile', 'PyYAML', 'twitter', 'discount'], 53 | 'mako': ['mako>=0.7'], 54 | }, 55 | tests_require=['Attest-latest', 'cram', 'docutils'], 56 | test_loader='attest:auto_reporter.test_loader', 57 | test_suite='specs.testsuite', 58 | entry_points={ 59 | 'console_scripts': 60 | ['acrylamid = acrylamid:acryl'] 61 | } 62 | ) 63 | -------------------------------------------------------------------------------- /specs/search.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import attest 6 | 7 | tt = attest.Tests() 8 | from acrylamid.views import search 9 | 10 | 11 | @tt.test 12 | def commonprefix(): 13 | 14 | for a, b, i in ('foo', 'faa', 1), ('test', 'test', 4), ('', 'spam', 0), ('a', 'b', 0): 15 | assert search.commonprefix(a, b) == (i, b) 16 | 17 | 18 | @tt.test 19 | def basics(): 20 | 21 | tree = {} 22 | for word in 'javascript', 'java', 'java-vm': 23 | search.insert(tree, word, 1) 24 | 25 | assert 'j' in tree 26 | assert 'ava' in tree['j'][0] 27 | assert 'script' in tree['j'][0]['ava'][0] 28 | assert '-vm' in tree['j'][0]['ava'][0] 29 | 30 | assert len(tree['j'][0]['ava']) == 2 # Java found! 31 | assert len(tree['j'][0]['ava'][0]['script']) == 2 # JavaScript found! 32 | assert len(tree['j'][0]['ava'][0]['-vm']) == 2 # Java-VM found! 33 | 34 | 35 | @tt.test 36 | def split(): 37 | 38 | tree = {} 39 | for word in 'a', 'aa', 'aaa', 'aaaa', 'ab': 40 | search.insert(tree, word, 1) 41 | 42 | assert 'a' in tree 43 | assert 'a' in tree['a'][0] 44 | assert 'a' in tree['a'][0]['a'][0] 45 | assert 'a' in tree['a'][0]['a'][0]['a'][0] 46 | assert 'b' in tree['a'][0] 47 | 48 | assert len(tree['a']) == 1 # search word must be longer than three chars ;) 49 | assert len(tree['a'][0]['a']) == 2 50 | assert len(tree['a'][0]['b']) == 2 51 | assert len(tree['a'][0]['a'][0]['a']) == 2 52 | assert len(tree['a'][0]['a'][0]['a'][0]['a']) == 2 53 | 54 | 55 | @tt.test 56 | def advanced(): 57 | 58 | def find(node, i): 59 | if len(node) == 2 and node[1] == i: 60 | yield i 61 | 62 | for key in node[0]: 63 | yield find(node[0][key], i) 64 | 65 | tree = {} 66 | words = 'eines', 'erwachte', 'er', 'einem', 'ein', 'erhalten', 'es', \ 67 | 'etwas', 'eine', 'einer', 'entgegenhob' 68 | 69 | for i, word in enumerate(words): 70 | search.insert(tree, word, i) 71 | 72 | for i in range(len(word)): 73 | assert len(list(find((tree, -1), i))) == 1 74 | 75 | if __name__ == '__main__': 76 | 77 | tt.run() 78 | -------------------------------------------------------------------------------- /acrylamid/tasks/deploy.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 os 10 | import argparse 11 | import subprocess 12 | 13 | from acrylamid import log 14 | from acrylamid.tasks import argument, task 15 | from acrylamid.errors import AcrylamidException 16 | from acrylamid.compat import iterkeys, iteritems, string_types, PY2K 17 | 18 | arguments = [ 19 | argument("task", nargs="?"), 20 | argument("args", nargs=argparse.REMAINDER), 21 | argument("--list", dest="list", action="store_true", default=False, 22 | help="list available tasks") 23 | ] 24 | 25 | 26 | @task(['deploy', 'dp'], arguments, help="run task") 27 | def run(conf, env, options): 28 | """Subcommand: deploy -- run the shell command specified in 29 | DEPLOYMENT[task] using Popen. Each string value from :doc:`conf.py` is 30 | added to the execution environment. Every argument after ``acrylamid 31 | deploy task ARG1 ARG2`` is appended to cmd.""" 32 | 33 | if options.list: 34 | for task in iterkeys(conf.get('deployment', {})): 35 | print(task) 36 | sys.exit(0) 37 | 38 | task, args = options.task or 'default', options.args 39 | cmd = conf.get('deployment', {}).get(task, None) 40 | 41 | if not cmd: 42 | raise AcrylamidException('no tasks named %r in conf.py' % task) 43 | 44 | # apply ARG1 ARG2 ... and -v --long-args to the command, e.g.: 45 | # $> acrylamid deploy task arg1 -b --foo 46 | cmd += ' ' + ' '.join(args) 47 | 48 | enc = sys.getfilesystemencoding() 49 | env = os.environ 50 | env.update(dict([(k.upper(), v.encode(enc, 'replace') if PY2K else v) 51 | for k, v in iteritems(conf) if isinstance(v, string_types)])) 52 | 53 | log.info('execute %s', cmd) 54 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 55 | 56 | while True: 57 | output = p.stdout.read(1) 58 | if output == b'' and p.poll() != None: 59 | break 60 | if output != b'': 61 | sys.stdout.write(output.decode(enc)) 62 | sys.stdout.flush() 63 | -------------------------------------------------------------------------------- /acrylamid/refs.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 functools import partial 7 | from itertools import chain 8 | from collections import defaultdict 9 | 10 | from acrylamid.core import cache 11 | from acrylamid.utils import hash 12 | from acrylamid.compat import map 13 | 14 | __orig_refs = None 15 | __seen_refs = None 16 | __entry_map = None 17 | 18 | 19 | def load(*entries): 20 | """Initialize references, load previous state.""" 21 | global __orig_refs, __seen_refs, __entry_map 22 | 23 | __seen_refs = defaultdict(set) 24 | __orig_refs = cache.memoize('references') or defaultdict(set) 25 | __entry_map = dict((hash(entry), entry) for entry in chain(*entries)) 26 | 27 | 28 | def save(): 29 | """Save new references state to disk.""" 30 | global __seen_refs 31 | cache.memoize('references', __seen_refs) 32 | 33 | 34 | def modified(key, references): 35 | """Check whether an entry hash `key` has modified `references`. This 36 | function takes the return values from :func:`refernces`.""" 37 | 38 | global __orig_refs, __entry_map 39 | 40 | if not references: 41 | return False 42 | 43 | if __orig_refs[key] != __seen_refs[key]: 44 | return True 45 | 46 | try: 47 | return any(__entry_map[ref].modified for ref in references) 48 | except KeyError: 49 | return True 50 | 51 | 52 | def references(entry): 53 | """Return hash for entry and the referenced entries' hashes.""" 54 | global __seen_refs 55 | return hash(entry), __seen_refs.get(hash(entry), set()) 56 | 57 | 58 | def track(func): 59 | """A syntactic-sugar decorator to automatically track yielded 60 | references from an entry. See :class:`Translation` in 61 | :mod:`acrylamid.views.entry` for an example.""" 62 | 63 | def dec(entry, item): 64 | append(entry, item) 65 | return item 66 | 67 | return lambda entry, **kw: map(partial(dec, entry), func(entry, **kw)) 68 | 69 | 70 | def append(entry, *references): 71 | """Appenf `references` to `entry`.""" 72 | global __seen_refs 73 | 74 | for ref in references: 75 | __seen_refs[hash(entry)].add(hash(ref)) 76 | -------------------------------------------------------------------------------- /acrylamid/filters/rstx_gist.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*-# 2 | # 3 | # License: This document has been placed in the public domain 4 | # Author: Brian Hsu 5 | 6 | from docutils.parsers.rst import Directive, directives 7 | from docutils import nodes 8 | 9 | from acrylamid.lib.requests import get, HTTPError, URLError 10 | from acrylamid import log 11 | 12 | class Gist(Directive): 13 | """`GitHub:Gist `__ embedding (file is optional). 14 | 15 | .. code-block:: rst 16 | 17 | .. gist:: 4145152 18 | :file: transmission.rb 19 | """ 20 | 21 | required_arguments = 1 22 | optional_arguments = 1 23 | option_spec = {'file': directives.unchanged} 24 | final_argument_whitespace = True 25 | has_content = False 26 | 27 | def get_raw_gist_with_filename(self, gistID, filename): 28 | url = "https://raw.github.com/gist/%s/%s" % (gistID, filename) 29 | try: 30 | return get(url).read() 31 | except (URLError, HTTPError) as e: 32 | log.exception('Failed to access URL %s : %s' % (url, e)) 33 | return '' 34 | 35 | def get_raw_gist(self, gistID): 36 | url = "https://raw.github.com/gist/%s" % (gistID) 37 | try: 38 | return get(url).read() 39 | except (URLError, HTTPError) as e: 40 | log.exception('Failed to access URL %s : %s' % (url, e)) 41 | return '' 42 | 43 | def run(self): 44 | 45 | gistID = self.arguments[0].strip() 46 | 47 | if 'file' in self.options: 48 | filename = self.options['file'] 49 | rawGist = (self.get_raw_gist_with_filename(gistID, filename)) 50 | embedHTML = '' % \ 51 | (gistID, filename) 52 | else: 53 | rawGist = (self.get_raw_gist(gistID)) 54 | embedHTML = '' % gistID 55 | 56 | return [nodes.raw('', embedHTML, format='html'), 57 | nodes.raw('', '', format='html')] 60 | 61 | 62 | def register(roles, directives): 63 | directives.register_directive('gist', Gist) 64 | -------------------------------------------------------------------------------- /acrylamid/filters/rst.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 sys 7 | import os 8 | import imp 9 | import traceback 10 | 11 | from distutils.version import LooseVersion 12 | 13 | from acrylamid import log 14 | from acrylamid.filters import Filter, discover 15 | 16 | try: 17 | from docutils.core import publish_parts, __version__ as version 18 | from docutils.parsers.rst import roles, directives 19 | except ImportError: 20 | publish_parts = roles = directives = None # NOQA 21 | 22 | 23 | class Restructuredtext(Filter): 24 | 25 | match = ['restructuredtext', 'rst', 'rest', 'reST', 'reStructuredText'] 26 | version = 2 27 | 28 | conflicts = ['markdown', 'plain'] 29 | priority = 70.00 30 | 31 | def init(self, conf, env): 32 | 33 | self.extensions = {} 34 | self.ignore = env.options.ignore 35 | 36 | if not publish_parts or not directives: 37 | raise ImportError(u'reStructuredText: No module named docutils') 38 | 39 | if not tuple(LooseVersion(version).version) > (0, 9): 40 | raise ImportError(u'docutils ≥ 0.9 required.') 41 | 42 | # -- discover reStructuredText extensions -- 43 | directories = conf['filters_dir'] + [os.path.dirname(__file__)] 44 | for filename in discover(directories, lambda path: path.startswith('rstx_')): 45 | modname, ext = os.path.splitext(os.path.basename(filename)) 46 | fp, path, descr = imp.find_module(modname, directories) 47 | 48 | try: 49 | mod = imp.load_module(modname, fp, path, descr) 50 | mod.register(roles, directives) 51 | except (ImportError, Exception) as e: 52 | traceback.print_exc(file=sys.stdout) 53 | log.warn('%r %s: %s' % (filename, e.__class__.__name__, e)) 54 | 55 | def transform(self, content, entry, *filters): 56 | 57 | settings = { 58 | 'initial_header_level': 1, 59 | 'doctitle_xform': 0, 60 | 'syntax_highlight': 'short' 61 | } 62 | 63 | parts = publish_parts(content, writer_name='html', settings_overrides=settings) 64 | return parts['body'] 65 | -------------------------------------------------------------------------------- /acrylamid/filters/rstx_youtube.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 docutils import nodes 7 | from docutils.parsers.rst import Directive, directives 8 | 9 | 10 | def align(argument): 11 | return directives.choice(argument, ('left', 'center', 'right')) 12 | 13 | 14 | class YouTube(Directive): 15 | """YouTube directive for easy embedding (`:options:` are optional). 16 | 17 | .. code-block:: rst 18 | 19 | .. youtube:: ZPJlyRv_IGI 20 | :start: 34 21 | :align: center 22 | :height: 1280 23 | :width: 720 24 | :privacy: 25 | :ssl: 26 | """ 27 | 28 | required_arguments = 1 29 | optional_arguments = 0 30 | option_spec = { 31 | 'height': directives.length_or_unitless, 32 | 'width': directives.length_or_percentage_or_unitless, 33 | 'border': directives.length_or_unitless, 34 | 'align': align, 35 | 'start': int, 36 | 'ssl': directives.flag, 37 | 'privacy': directives.flag 38 | } 39 | has_content = False 40 | 41 | def run(self): 42 | 43 | alignments = { 44 | 'left': '0', 45 | 'center': '0 auto', 46 | 'right': '0 0 0 auto', 47 | } 48 | 49 | uri = ('https://' if 'ssl' in self.options else 'http://') \ 50 | + ('www.youtube-nocookie.com' if 'privacy' in 51 | self.options else 'www.youtube.com') \ 52 | + '/embed/' + self.arguments[0] 53 | self.options['uri'] = uri 54 | self.options['align'] = alignments[self.options.get('align', 'center')] 55 | self.options.setdefault('width', '680px') 56 | self.options.setdefault('height', '382px') 57 | self.options.setdefault('border', 0) 58 | self.options.setdefault('start', 0) 59 | 60 | YT_EMBED = """""" 63 | return [nodes.raw('', YT_EMBED % self.options, format='html')] 64 | 65 | 66 | def register(roles, directives): 67 | for name in 'youtube', 'yt': 68 | directives.register_directive(name, YouTube) 69 | -------------------------------------------------------------------------------- /docs/assets.rst: -------------------------------------------------------------------------------- 1 | Assets 2 | ====== 3 | 4 | A web log merely consists of text only, hence you might want CSS -- or even 5 | more advanced -- SCSS or LESS to style your content, use some fancy JavaScript 6 | or just include an image. These are all assets. 7 | 8 | By convention, you put your static files into the `static/` folder, but you 9 | may change this via ``STATIC = "/path/to/dir"`` in your :doc:`conf.py`. 10 | Without extensions, Acrylamid will just copy the content from your static 11 | directory to the output directory, the same applies to your assets located 12 | in your `THEME` folder. 13 | 14 | The Concept of Writers 15 | ---------------------- 16 | 17 | If you have several static pages, that do not belong to your blog, you can use 18 | them as plain HTML files in your asset folder and you have the ability to use 19 | your prefered templating language here. 20 | 21 | Available Writers 22 | ^^^^^^^^^^^^^^^^^ 23 | 24 | Template : .j2, .mako or .html -> .html 25 | renders HTML (and engine specific extensions) with your current templating 26 | engine. You can inherit from your theme directory as well from all 27 | templates inside your static directory. 28 | 29 | This writer is activated by default. 30 | 31 | HTML : .html -> .html 32 | Copy plain HTML files to output if not in theme directory. 33 | 34 | XML : .xml -> .xml 35 | Same as the HTML writer but for XML. 36 | 37 | Webassets Integration 38 | --------------------- 39 | 40 | To handle SASS, SCSS and LESS (and much more) Acrylamid uses the Webassets_ 41 | project. To use Webassets_ you first need to install the egg via:: 42 | 43 | $ easy_install "webassets<0.10" 44 | 45 | and you need a working SASS, LESS or whatever-you-want compiler. Next you 46 | define your assets in your template like this: 47 | 48 | .. code-block:: html+jinja 49 | 50 | {% for url in compile('style.scss', filters="scss", output="style.%(version)s.css", 51 | depends=["scss/*.css", "scss/*.scss"]) %} 52 | 53 | {% endfor %} 54 | 55 | This will compile the master `theme/style.scss` file using SCSS_ to the 56 | specified output (the ``%(version).s`` is a placeholder for the version hash). 57 | Use the `depends` keyword to watch additional files for changes (the `import` 58 | clause within a stylesheet does not work yet). 59 | 60 | .. _webassets: http://webassets.readthedocs.org/en/latest/index.html 61 | .. _SCSS: http://sass-lang.com/ 62 | -------------------------------------------------------------------------------- /acrylamid/filters/relative.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 acrylamid import log 7 | from acrylamid.filters import Filter 8 | from acrylamid.helpers import joinurl 9 | from acrylamid.lib.html import HTMLParser 10 | 11 | 12 | class Href(HTMLParser): 13 | 14 | def __init__(self, html, func=lambda part: part): 15 | self.func = func 16 | super(Href, self).__init__(html) 17 | 18 | def apply(self, attrs): 19 | 20 | for i, (key, value) in enumerate(attrs): 21 | if key in ('href', 'src'): 22 | attrs[i] = (key, self.func(value)) 23 | 24 | return attrs 25 | 26 | def handle_starttag(self, tag, attrs): 27 | if tag == 'a': 28 | attrs = self.apply(attrs) 29 | super(Href, self).handle_starttag(tag, attrs) 30 | 31 | def handle_startendtag(self, tag, attrs): 32 | if tag == 'img': 33 | attrs = self.apply(attrs) 34 | super(Href, self).handle_startendtag(tag, attrs) 35 | 36 | 37 | class Relative(Filter): 38 | 39 | match = ['relative'] 40 | version = 1 41 | priority = 15.0 42 | 43 | def transform(self, text, entry, *args): 44 | 45 | def relatively(part): 46 | 47 | if part.startswith('/') or part.find('://') == part.find('/') - 1: 48 | return part 49 | 50 | return joinurl(entry.permalink, part) 51 | 52 | try: 53 | return ''.join(Href(text, relatively).result) 54 | except: 55 | log.warn('%s: %s in %s' % (e.__class__.__name__, e.msg, entry.filename)) 56 | return text 57 | 58 | 59 | class Absolute(Filter): 60 | 61 | match = ['absolute'] 62 | version = 2 63 | priority = 15.0 64 | 65 | @property 66 | def uses(self): 67 | return self.conf.www_root 68 | 69 | def transform(self, text, entry, *args): 70 | 71 | def absolutify(part): 72 | 73 | if part.startswith('/'): 74 | return self.conf.www_root + part 75 | 76 | if part.find('://') == part.find('/') - 1: 77 | return part 78 | 79 | return self.conf.www_root + joinurl(entry.permalink, part) 80 | 81 | try: 82 | return ''.join(Href(text, absolutify).result) 83 | except: 84 | log.warn('%s: %s in %s' % (e.__class__.__name__, e.msg, entry.filename)) 85 | return text 86 | -------------------------------------------------------------------------------- /docs/filters/markup/other.rst: -------------------------------------------------------------------------------- 1 | HTML, Textile, Pandoc 2 | ===================== 3 | 4 | HTML 5 | ---- 6 | 7 | No transformation applied. Useful if your text is already written in HTML. 8 | 9 | ============ ================================================== 10 | Requires 11 | Aliases pass, plain, html, xhtml, HTML 12 | Conflicts reStructuredText, Markdown, Pandoc 13 | ============ ================================================== 14 | 15 | textile 16 | ------- 17 | 18 | A *textile* filter if like the textile_ markup language. Note, that the `python 19 | implementation`_ of Textile has been not actively maintained for more than a 20 | year. Textile is the only text processor so far that automatically adds 21 | typographical enhancements to the generated HTML (but not all applied from 22 | :ref:`filters-post-typography`). 23 | 24 | .. _textile: https://en.wikipedia.org/wiki/Textile_%28markup_language%29 25 | .. _python implementation: https://github.com/sebix/python-textile 26 | 27 | ============ ================================================== 28 | Requires ``textile`` 29 | Aliases Textile, textile, pytextile, PyTextile 30 | Conflicts HTML, Markdown, Pandoc, reStructuredText 31 | ============ ================================================== 32 | 33 | pandoc 34 | ------ 35 | 36 | This is filter is a universal converter for various markup language such as 37 | Markdown, reStructuredText, Textile and LaTeX (including special extensions by 38 | pandoc) to HTML. A typical call would look like 39 | ``filters: [pandoc+Markdown+mathml+...]``. 40 | 41 | You can write your posts with `pandoc's title block`_:: 42 | 43 | % Title 44 | % Author 45 | 46 | You can find a complete list of pandocs improved (and bugfixed) Markdown 47 | implementation in the `Pandoc User's Guide`_. 48 | 49 | .. _Pandoc's title block: http://johnmacfarlane.net/pandoc/README.html#title-block> 50 | .. _Pandoc User's Guide: http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown 51 | 52 | ============ ================================================== 53 | Requires `Pandoc – a universal document converter 54 | `_ in PATH 55 | Aliases Pandoc, pandoc 56 | Conflicts reStructuredText, HTML, Markdown 57 | Arguments First argument is the FORMAT like Markdown, 58 | textile and so on. All arguments after that are 59 | applied as additional long-opts to pandoc. 60 | ============ ================================================== 61 | 62 | -------------------------------------------------------------------------------- /acrylamid/filters/mdx_superscript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2010, Shane Graber 4 | # 5 | # Superscipt extension for Markdown. 6 | # 7 | # To superscript something, place a carat symbol, '^', before and after the 8 | # text that you would like in superscript: 6.02 x 10^23^ 9 | # The '23' in this example will be superscripted. See below. 10 | # 11 | # Examples: 12 | # 13 | # >>> import markdown 14 | # >>> md = markdown.Markdown(extensions=['superscript']) 15 | # >>> md.convert('This is a reference to a footnote^1^.') 16 | # u'

This 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 Zimmermann . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | 6 | import os 7 | import imp 8 | import markdown 9 | 10 | from acrylamid.errors import AcrylamidException 11 | from acrylamid.compat import string_types 12 | from acrylamid.filters import Filter, discover 13 | 14 | 15 | class Markdown(Filter): 16 | 17 | match = ['md', 'mkdown', 'markdown', 'Markdown'] 18 | version = 2 19 | 20 | conflicts = ['rst', 'plain'] 21 | priority = 70.0 22 | 23 | extensions = dict((x, x) for x in ['abbr', 'fenced_code', 'footnotes', 'headerid', 24 | 'tables', 'codehilite', 'def_list', 'extra', 'smart_strong', 'nl2br', 25 | 'sane_lists', 'wikilink', 'attr_list']) 26 | 27 | def init(self, conf, env): 28 | 29 | self.failed = [] 30 | self.ignore = env.options.ignore 31 | 32 | markdown.Markdown # raises ImportError eventually 33 | 34 | # -- discover markdown extensions -- 35 | directories = conf['filters_dir'] + [os.path.dirname(__file__)] 36 | for filename in discover(directories, lambda path: path.startswith('mdx_')): 37 | modname, ext = os.path.splitext(os.path.basename(filename)) 38 | fp, path, descr = imp.find_module(modname, directories) 39 | 40 | try: 41 | mod = imp.load_module(modname, fp, path, descr) 42 | mdx = mod.makeExtension() 43 | if isinstance(mod.match, string_types): 44 | mod.match = [mod.match] 45 | for name in mod.match: 46 | self.extensions[name] = mdx 47 | except (ImportError, Exception) as e: 48 | self.failed.append('%r %s: %s' % (filename, e.__class__.__name__, e)) 49 | 50 | def __contains__(self, key): 51 | return True if key in self.extensions else False 52 | 53 | def transform(self, text, entry, *filters): 54 | 55 | val = [] 56 | for f in filters: 57 | if f in self: 58 | val.append(f) 59 | else: 60 | x = f.split('(', 1)[:1][0] 61 | if x in self: 62 | val.append(x) 63 | self.extensions[x] = f 64 | elif not self.ignore: 65 | raise AcrylamidException('Markdown: %s' % '\n'.join(self.failed)) 66 | 67 | return markdown.Markdown( 68 | extensions=[self.extensions[m] for m in val], 69 | output_format='xhtml5' 70 | ).convert(text) 71 | -------------------------------------------------------------------------------- /docs/views/index.rst: -------------------------------------------------------------------------------- 1 | Views 2 | ===== 3 | 4 | Now, you know about transforming your content, you'll learn how to aggregate 5 | your content. Acrylamid ships a few views but you are not limited to them. You 6 | can easily write your own view without forking the project (more on this topic 7 | later). Currently, Acrylamid ships the following: 8 | 9 | - article overview, an aggregation of all posts grouped by year and month. 10 | - entry view, a single full-text post. 11 | - index view, pagination of your content. 12 | - tag view, per tag pagination. 13 | - feed view, offering RSS and Atom aggregation (also per tag). 14 | - static site search, self-explanatory. 15 | 16 | All views have some properties in common such as path, filters and conditionals, 17 | you've to set in your :doc:`/conf.py`. The idea of views is similar to routes in 18 | Django or Flask. You can set your URL setup to whatever you like, Acrylamid is 19 | not fixed to a single directory structure. For convenience, Acrylamid appends an 20 | ``index.html`` to a URL if it does end with a slash (as shown in the defaults). 21 | 22 | All views share some options such as view type, conditionals or route in common, 23 | but can also offer individual parameters, you'll find explained in the built-in 24 | views section. 25 | 26 | path : string 27 | The route/path of the view(s). Note that you can't add the same route a second 28 | time, but you can add multiple views to a path. 29 | 30 | view : string 31 | Name of your wished view. Exact case does not matter that much. 32 | 33 | views : list of strings 34 | A list of views as described above. 35 | 36 | filters : string or list of strings 37 | A list of filters or a single filter name to apply to the view. 38 | 39 | if : lambda/function 40 | A condition to filter your content before they are passed to the view. 41 | 42 | Here's an example of how to use views: 43 | 44 | .. code-block:: python 45 | 46 | VIEWS = { 47 | "/path/": {"view": "translation", "if": lambda e: e.lang == 'klingon'} 48 | } 49 | 50 | To see, what variables are available during templating, consult :doc:`/templating`. 51 | 52 | 53 | Built-in Views 54 | ************** 55 | 56 | * :ref:`views-entry`, :ref:`views-page`, :ref:`views-translation` 57 | * :ref:`views-archive`, :ref:`views-tag`, :ref:`views-category` 58 | * :doc:`/views/feeds`, 59 | * :ref:`views-sitemap` 60 | * :doc:`/views/search` 61 | 62 | Custom Views 63 | ************ 64 | 65 | You can easily extend Acrylamid by writing custom views directly in your blog 66 | directory. Just add ``VIEWS_DIR += ['views/']`` to your :doc:`/conf.py` and write 67 | your own view. 68 | -------------------------------------------------------------------------------- /docs/howtos.rst: -------------------------------------------------------------------------------- 1 | Knowledge base 2 | ============== 3 | 4 | A single per-tag Feed 5 | ********************* 6 | 7 | A single feed pretty easy, just add this into your *conf.py*: 8 | 9 | .. code-block:: python 10 | 11 | '/my/feed': {'view': 'feed', 'if': lambda e: 'whatever' in e.tags} 12 | 13 | To have a feed for each tag, use the newish Atom/RSS per tag view (a 14 | configuration example is in your ``conf.py``). 15 | 16 | Image Gallery 17 | ************* 18 | 19 | Acrylamid does not ship an image gallery and will properbly never do -- it's 20 | too complex to fit to everyones need. But that does not mean, you can not have 21 | a automated image gallery. You can write: 22 | 23 | .. code-block:: html+jinja 24 | 25 | {% set images = "ls output/img/2012/st-petersburg/*.jpg" | cut -d / -f 5" %} 26 | {% for bunch in images | system | split | batch(num) %} 27 |
28 | {% for file in bunch %} 29 | 30 | {{ file }} 32 | 33 | {% endfor %} 34 |
35 |
36 | {% endfor %} 37 | 38 | this into your jinja2-enabled post (= ``filter: jinja2``) and make sure you 39 | point to your image location. To convert thumbnails from your images, you 40 | can use `ImageMagick's`_ convert to create 150x150 px thumbnails in *thumbs/*: 41 | 42 | .. code-block:: bash 43 | 44 | $ for file in `ls *.jpg`; do 45 | > convert -define jpeg:size=300x300 $file -thumbnail 150x150^ -gravity center -extent 150x150 "thumbs/$file"; 46 | > done 47 | 48 | .. _ImageMagick's: http://www.imagemagick.org/ 49 | 50 | That will look similar to my blog article about `St. Petersburg `_. 51 | 52 | Performance Tweaks 53 | ****************** 54 | 55 | Markdown instead of reStructuredText as markup language might be faster. Using 56 | a native Markdown compiler such as Discount is even faster. Another important 57 | factor is the typography-filter (disabled by default) which consumes about 40% 58 | of the whole compilation process. If you don't care about web typography, 59 | disable this feature gives you a huge performance boost. 60 | 61 | A short list from slow filters (slowest to less slower): 62 | 63 | 1. Typography 64 | 2. Acronyms 65 | 3. reStructuredText 66 | 4. Hyphenation 67 | 68 | Though reStructuredText is not *that* slow, it takes about 300 ms just to 69 | initialize on import. Typography as well as Acronyms and Hyphenation are 70 | limited by their underlying library, namely :class:`HTMLParser` and :class:`re`. 71 | -------------------------------------------------------------------------------- /acrylamid/lib/httpd.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 | Internal Webserver 8 | ~~~~~~~~~~~~~~~~~~ 9 | 10 | Launch a dumb webserver as thread.""" 11 | 12 | import os 13 | import time 14 | 15 | from threading import Thread 16 | 17 | from acrylamid.utils import force_unicode as u 18 | from acrylamid.compat import PY2K 19 | from acrylamid.helpers import joinurl 20 | 21 | if PY2K: 22 | from SocketServer import TCPServer 23 | from SimpleHTTPServer import SimpleHTTPRequestHandler 24 | else: 25 | from socketserver import TCPServer 26 | from http.server import SimpleHTTPRequestHandler 27 | 28 | 29 | class ReuseAddressServer(TCPServer): 30 | """avoids socket.error: [Errno 48] Address already in use""" 31 | allow_reuse_address = True 32 | 33 | def serve_forever(self): 34 | """Handle one request at a time until doomsday.""" 35 | while not self.kill_received: 36 | if not self.wait: 37 | self.handle_request() 38 | else: 39 | time.sleep(0.1) 40 | 41 | 42 | class RequestHandler(SimpleHTTPRequestHandler): 43 | """This is a modified version of python's -m SimpleHTTPServer to 44 | serve on a specific sub directory of :func:`os.getcwd`.""" 45 | 46 | www_root = '.' 47 | log_error = lambda x, *y: None 48 | 49 | def translate_path(self, path): 50 | path = SimpleHTTPRequestHandler.translate_path(self, path) 51 | return joinurl(u(os.getcwd()), self.www_root, path[len(u(os.getcwd())):]) 52 | 53 | def end_headers(self): 54 | self.send_header("Cache-Control", "max-age=0, must-revalidate") 55 | SimpleHTTPRequestHandler.end_headers(self) 56 | 57 | 58 | class Webserver(Thread): 59 | """A single-threaded webserver to serve while generation. 60 | 61 | :param port: port to listen on 62 | :param root: serve this directory under /""" 63 | 64 | def __init__(self, port=8000, root='.', log_message=lambda x, *y: None): 65 | Thread.__init__(self) 66 | Handler = RequestHandler 67 | Handler.www_root = root 68 | Handler.log_message = log_message 69 | 70 | self.httpd = ReuseAddressServer(("", port), Handler) 71 | self.httpd.wait = False 72 | self.httpd.kill_received = False 73 | 74 | def setwait(self, value): 75 | self.httpd.wait = value 76 | wait = property(lambda self: self.httpd.wait, setwait) 77 | 78 | def run(self): 79 | self.httpd.serve_forever() 80 | self.join(1) 81 | 82 | def shutdown(self): 83 | """"Sets kill_recieved and closes the server socket.""" 84 | self.httpd.kill_received = True 85 | self.httpd.socket.close() 86 | -------------------------------------------------------------------------------- /acrylamid/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2012 Martin Zimmermann . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | # 6 | # acrylamid.tasks can add additional tasks to argument parser and execution 7 | 8 | import argparse 9 | 10 | from os.path import dirname 11 | from acrylamid.compat import string_types 12 | from acrylamid.helpers import discover 13 | 14 | # we get them from acrylamid/__init__.py 15 | subparsers, default = None, None 16 | 17 | # here we collect aliases to their function 18 | collected = {} 19 | 20 | 21 | def initialize(_subparsers, _default, ext_dir='tasks/'): 22 | 23 | global subparsers, default 24 | subparsers, default = _subparsers, _default 25 | 26 | discover([dirname(__file__), ext_dir], lambda x: x, 27 | lambda path: path.rpartition('.')[0] != __file__.rpartition('.')[0]) 28 | 29 | 30 | def register(aliases, arguments=[], help=argparse.SUPPRESS, func=lambda *z: None, parents=True): 31 | """Add a task to a new subcommand parser, that integrates into `acrylamid --help`. 32 | 33 | :param aliases: a string or list of names for this task, the first is shown in ``--help`` 34 | :param arguments: a list of :func:`argument` 35 | :param help: short help about this command 36 | :param func: function to run when the user chooses this task 37 | :param parents: inherit default options like ``--verbose`` 38 | :type parents: True or False 39 | """ 40 | 41 | global subparsers, default, collected 42 | 43 | if isinstance(aliases, string_types): 44 | aliases = [aliases, ] 45 | 46 | if aliases[0] in collected: 47 | return 48 | 49 | parser = subparsers.add_parser( 50 | aliases[0], 51 | help=help, 52 | parents=[default] if parents else []) 53 | 54 | for arg in arguments: 55 | parser.add_argument(*arg.args, **arg.kwargs) 56 | 57 | for alias in aliases: 58 | subparsers._name_parser_map[alias] = parser 59 | collected[alias] = func 60 | 61 | 62 | class task(object): 63 | """A decorator to ease task creation. 64 | 65 | .. code-block:: python 66 | 67 | @task("hello", help="say hello") 68 | def hello(conf, env, options): 69 | 70 | print('Hello World!') 71 | """ 72 | 73 | def __init__(self, *args, **kwargs): 74 | 75 | self.args = args 76 | self.kwargs = kwargs 77 | 78 | def __call__(self, func): 79 | 80 | self.kwargs['func'] = func 81 | register(*self.args, **self.kwargs) 82 | 83 | 84 | def argument(*args, **kwargs): 85 | """A :func:`make_option`-like wrapper, use it to create your arguments:: 86 | 87 | arguments = [argument('-i', '--ini', nargs="+", default=0)]""" 88 | 89 | return type('Argument', (object, ), {'args': args, 'kwargs': kwargs}) 90 | -------------------------------------------------------------------------------- /acrylamid/filters/mdx_delins.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # - Copyright 2011, 2012 The Active Archives contributors 4 | # - Copyright 2011, 2012 Alexandre Leray 5 | # 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # 1. Redistributions of source code must retain the above copyright notice, this 12 | # list of conditions and the following disclaimer. 13 | # 14 | # 2. Redistributions in binary form must reproduce the above copyright notice, 15 | # this list of conditions and the following disclaimer in the documentation 16 | # and/or other materials provided with the distribution. 17 | # 18 | # 3. Neither the name of the nor the names of its contributors may 19 | # be used to endorse or promote products derived from this software without 20 | # specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY 23 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | # DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT 26 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 27 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 28 | # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 29 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 31 | # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | # 33 | # Del/Ins Extension for Python-Markdown 34 | # ===================================== 35 | # 36 | # Wraps the inline content with ins/del tags. 37 | # 38 | # 39 | # Usage 40 | # ----- 41 | # 42 | # >>> import markdown 43 | # >>> src = """This is ++added content++ and this is ~~deleted content~~""" 44 | # >>> html = markdown.markdown(src, ['del_ins']) 45 | # >>> print(html) 46 | #

This is added content and this is deleted content 47 | #

48 | 49 | import markdown 50 | from markdown.inlinepatterns import SimpleTagPattern 51 | 52 | match = ['delins'] 53 | 54 | 55 | class DelInsExtension(markdown.extensions.Extension): 56 | """Adds del_ins extension to Markdown class.""" 57 | 58 | def extendMarkdown(self, md, md_globals): 59 | """Modifies inline patterns.""" 60 | md.inlinePatterns.add('del', SimpleTagPattern(r"(\~\~)(.+?)(\~\~)", 'del'), '`__). 11 | 12 | To enable Entry view, add this to your :doc:`conf.py`: 13 | 14 | .. code-block:: python 15 | 16 | '/:year/:slug/': { 17 | 'view': 'entry', 18 | 'template': 'main.html' # default, includes entry.html 19 | } 20 | 21 | The entry view renders an post to a unique location and should be used as 22 | permalink URL. The url is user configurable, but may be overwritten by 23 | setting ``ENTRY_PERMALINK`` explicitly to a URL in your configuration. 24 | 25 | This view takes no other arguments and uses *main.html* and *entry.html* as 26 | template. 27 | 28 | .. _views-page: 29 | 30 | Page 31 | ---- 32 | 33 | Creates a static page. To enable Page view, add this to your :doc:`conf.py`: 34 | 35 | .. code-block:: python 36 | 37 | '/:slug/': { 38 | 'view': 'page', 39 | 'template': 'main.html' # default, includes entry.html 40 | } 41 | 42 | The page view renders an post to a unique location withouth any references 43 | to other blog entries. The url is user configurable, but may be overwritten by 44 | setting ``PAGE_PERMALINK`` explicitly to a URL in your configuration. 45 | 46 | This view takes no other arguments and uses *main.html* and *entry.html* as 47 | template. 48 | 49 | .. _views-translation: 50 | 51 | Translation 52 | ----------- 53 | 54 | Creates translation of a single full-length entry. To enable the 55 | Translation view, add this to your :doc:`conf.py`: 56 | 57 | .. code-block:: python 58 | 59 | '/:year/:slug/:lang/': { 60 | 'view': 'translation', 61 | 'template': 'main.html', # default, includes entry.html 62 | } 63 | 64 | Translations are posts with the same `identifier` and a different `lang` attribute. 65 | An example: 66 | 67 | The English article:: 68 | 69 | --- 70 | title: Foobar is not dead 71 | identifier: foobar-is-not-dead 72 | --- 73 | 74 | That's true, foobar is still alive! 75 | 76 | And the French version:: 77 | 78 | --- 79 | title: Foobar n'est pas mort ! 80 | identifier: foobar-is-not-dead 81 | lang: fr 82 | --- 83 | 84 | Oui oui, foobar est toujours vivant ! 85 | 86 | If the blog language is ``"en"`` then the english article will be included into 87 | the default listing but the french version not. You can link to the translated 88 | versions via: 89 | 90 | .. code-block:: html+jinja 91 | 92 | {% if 'translation' in env.views and env.translationsfor(entry) %} 93 |
    94 | {% for tr in env.translationsfor(entry) %} 95 |
  • {{ tr.lang }}: 96 | {{ tr.title }} 97 |
  • 98 | {% endfor %} 99 |
100 | {% endif %} 101 | -------------------------------------------------------------------------------- /acrylamid/defaults.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import io 6 | from os.path import join, dirname 7 | 8 | from acrylamid import log, compat 9 | 10 | copy = lambda path: io.open(join(dirname(__file__), path), 'rb') 11 | 12 | __ = ['*.swp', ] 13 | 14 | conf = { 15 | 'sitename': 'A descriptive blog title', 16 | 'author': 'Anonymous', 17 | 'email': 'info@example.com', 18 | 19 | 'date_format': '%d.%m.%Y, %H:%M', 20 | 'encoding': 'utf-8', 21 | 'permalink_format': '/:year/:slug/', 22 | 23 | # pagination 24 | 'default_orphans': 0, 25 | 26 | # tag cloud 27 | 'tag_cloud_max_items': 100, 28 | 'tag_cloud_steps': 4, 29 | 'tag_cloud_start_index': 0, 30 | 'tag_cloud_shuffle': False, 31 | 32 | # filter & view configuration 33 | 'filters_dir': [], 34 | 'views_dir': [], 35 | 36 | 'filters': ['markdown+codehilite(css_class=highlight)', 'hyphenate'], 37 | 'views': { 38 | }, 39 | 40 | # user dirs 41 | 'output_dir': 'output/', 42 | 'output_ignore': ['.git*', '.hg*', '.svn'], 43 | 44 | 'content_dir': 'content/', 45 | 'content_ignore': ['.git*', '.hg*', '.svn'] + __, 46 | 'content_extension': ['.txt', '.rst', '.md'], 47 | 48 | 'theme': 'layouts/', 49 | 'theme_ignore': ['.git*', '.hg*', '.svn'] + __, 50 | 51 | 'static': None, 52 | 'static_ignore': ['.git*', '.hg*', '.svn'] + __, 53 | 'static_filter': ['Template', 'XML'], 54 | 55 | 'engine': 'acrylamid.templates.jinja2.Environment', 56 | } 57 | 58 | 59 | def normalize(conf): 60 | 61 | # metastyle has been removed 62 | if 'metastyle' in conf: 63 | log.info('notice METASTYLE is no longer needed to determine the metadata format ' + \ 64 | 'and can be removed.') 65 | 66 | # deprecated since 0.8 67 | if isinstance(conf['static'], list): 68 | conf['static'] = conf['static'][0] 69 | log.warn("multiple static directories has been deprecated, " + \ 70 | "Acrylamid continues with '%s'.", conf['static']) 71 | 72 | # deprecated since 0.8 73 | for fx in 'Jinja2', 'Mako': 74 | try: 75 | conf['static_filter'].remove(fx) 76 | except ValueError: 77 | pass 78 | else: 79 | log.warn("%s asset filter has been renamed to `Template` and is " 80 | "included by default.", fx) 81 | 82 | if not isinstance(conf['theme'], list): 83 | conf['theme'] = [conf['theme']] 84 | 85 | for i, path in enumerate(conf['theme']): 86 | if not path.endswith('/'): 87 | conf['theme'][i] = path + "/" 88 | 89 | for key in 'content_dir', 'static', 'output_dir': 90 | if conf[key] is not None and not conf[key].endswith('/'): 91 | conf[key] += '/' 92 | 93 | for key in 'views_dir', 'filters_dir': 94 | if isinstance(conf[key], compat.string_types): 95 | conf[key] = [conf[key], ] 96 | 97 | return conf 98 | -------------------------------------------------------------------------------- /acrylamid/lib/history.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright 2012 Martin Zimmermann . All rights reserved. 4 | # License: BSD Style, 2 clauses -- see LICENSE. 5 | # 6 | # give update information for Acrylamid 7 | 8 | from __future__ import print_function 9 | 10 | import io 11 | import re 12 | 13 | from os.path import join, dirname 14 | 15 | from acrylamid.lib import __file__ as PATH 16 | from acrylamid.colors import blue, red, bold, underline 17 | from acrylamid.helpers import memoize 18 | 19 | 20 | def changesfor(version): 21 | """return CHANGES for `version` and whether it *breaks*.""" 22 | 23 | with io.open(join(dirname(PATH), 'CHANGES'), encoding='utf-8') as fp: 24 | 25 | rv = [] 26 | section, paragraph, safe = False, False, True 27 | 28 | for line in (line.rstrip() for line in fp if line): 29 | 30 | if not line: 31 | continue 32 | 33 | m = re.match(r'^(\d\.\d) \(\d{4}-\d{2}-\d{2}\)$', line) 34 | 35 | if m: 36 | section = m.group(1) == version 37 | continue 38 | 39 | if section and line.startswith('### '): 40 | paragraph = 'changes' in line 41 | continue 42 | 43 | if section and paragraph: 44 | rv.append(line) 45 | if 'break' in line: 46 | safe = False 47 | 48 | return not safe, '\n'.join(rv) 49 | 50 | 51 | colorize = lambda text: \ 52 | re.sub('`([^`]+)`', lambda m: bold(blue(m.group(1))).encode('utf-8'), 53 | re.sub('`([A-Z_*]+)`', lambda m: bold(m.group(1)).encode('utf-8'), 54 | re.sub('(#\d+)', lambda m: underline(m.group(1)).encode('utf-8'), 55 | re.sub('(breaks?)', lambda m: red(bold(m.group(1))).encode('utf-8'), text)))) 56 | 57 | 58 | def breaks(env, firstrun): 59 | """Return whether the new version may break current configuration and print 60 | all changes between the current and new version.""" 61 | 62 | version = memoize('version') or (0, 4) 63 | if version >= (env.version.major, env.version.minor): 64 | return False 65 | 66 | memoize('version', (env.version.major, env.version.minor)) 67 | 68 | if firstrun: 69 | return False 70 | 71 | broken = False 72 | 73 | for major in range(version[0], env.version.major or 1): 74 | for minor in range(version[1], env.version.minor): 75 | rv, hints = changesfor('%i.%i' % (major, minor + 1)) 76 | broken = broken or rv 77 | 78 | if not hints: 79 | continue 80 | 81 | print() 82 | print((blue('Acrylamid') + ' %i.%s' % (major, minor+1) + u' – changes').encode('utf-8'), end="") 83 | 84 | if broken: 85 | print((u'– ' + red('may break something.')).encode('utf-8')) 86 | else: 87 | print() 88 | 89 | print() 90 | print(colorize(hints).encode('utf-8')) 91 | print() 92 | 93 | return broken 94 | -------------------------------------------------------------------------------- /specs/filters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import attest 5 | 6 | from acrylamid.core import Configuration 7 | from acrylamid.filters import FilterList, FilterTree 8 | from acrylamid.filters import Filter, disable 9 | 10 | 11 | def build(name, **kw): 12 | return type(name, (Filter, ), kw)(Configuration({}), {}, name) 13 | 14 | 15 | class TestFilterlist(attest.TestBase): 16 | 17 | @attest.test 18 | def plain_strings(self): 19 | 20 | f1 = build('F1', match=['foo', 'bar'], conflicts=['spam']) 21 | f2 = build('F2', match=['spam']) 22 | f3 = build('F3', match=['bla']) 23 | 24 | x = FilterList([f1]) 25 | 26 | assert f1 in x 27 | assert f2 in x 28 | assert f3 not in x 29 | 30 | @attest.test 31 | def regex_strings(self): 32 | 33 | f1 = build('F1', match=['foo', 'bar'], conflicts=['spam']) 34 | f2 = build('F2', match=[re.compile('^spam$')]) 35 | f3 = build('F3', match=['bla']) 36 | 37 | x = FilterList([f1]) 38 | 39 | assert f1 in x 40 | assert f2 in x 41 | assert f3 not in x 42 | 43 | @attest.test 44 | def conflicts(self): 45 | 46 | f1 = build('F1', match=['foo', 'bar'], conflicts=['spam']) 47 | f4 = build('F4', match=['baz'], conflicts=['foo']) 48 | 49 | x = FilterList([f1]) 50 | 51 | assert f1 in x 52 | assert f4 in x 53 | 54 | @attest.test 55 | def access_by_name(self): 56 | 57 | f3 = build('F3', match=[re.compile('^sp', re.I)]) 58 | x = FilterList([f3]) 59 | 60 | assert x['sp'] == f3 61 | assert x['spam'] == f3 62 | assert x['sPaMmEr'] == f3 63 | 64 | @attest.test 65 | def disable(self): 66 | 67 | f1 = build('F1', match=['Foo'], conflicts=['Bar']) 68 | f2 = disable(f1) 69 | 70 | assert hash(f1) != hash(f2) 71 | assert f1.match != f2.match 72 | 73 | assert f1.name == f2.name 74 | assert f1.conflicts == f2.conflicts 75 | 76 | 77 | class TestFilterTree(attest.TestBase): 78 | pass 79 | # @attest.test 80 | # def path(self): 81 | 82 | # t = FilterTree() 83 | # t.add([1, 3, 4, 7], 'foo') 84 | # assert t.path('foo') == [1, 3, 4, 7] 85 | 86 | # @attest.test 87 | # def works(self): 88 | 89 | # t = FilterTree() 90 | 91 | # t.add([1, 2, 5], 'foo') 92 | # t.add([1, 2, 3, 5], 'bar') 93 | # t.add([7, ], 'baz') 94 | 95 | # assert list(t.iter('foo')) == [[1, 2], [5, ]] 96 | # assert list(t.iter('bar')) == [[1, 2], [3, 5]] 97 | # assert list(t.iter('baz')) == [[7, ], ] 98 | 99 | # @attest.test 100 | # def edge_cases(self): 101 | 102 | # t = FilterTree() 103 | 104 | # t.add([1, 2], 'foo') 105 | # t.add([1, 2], 'bar') 106 | # t.add([2, ], 'baz') 107 | 108 | # assert list(t.iter('foo')) == [[1, 2], ] 109 | # assert list(t.iter('bar')) == [[1, 2], ] 110 | # assert list(t.iter('baz')) == [[2, ], ] 111 | -------------------------------------------------------------------------------- /docs/filters/markup/rst.rst: -------------------------------------------------------------------------------- 1 | reStructuredText 2 | ================ 3 | 4 | reStructuredText (frequently abbreviated as reST) is an easy-to-read, 5 | what-you-see-is-what-you-get plaintext markup syntax and parser system. 6 | 7 | - `Project Website `__ 8 | - `Reference `__ 9 | - `Frequently Asked Questions `__ 10 | 11 | Implementation 12 | -------------- 13 | 14 | Because there is only a single implementation, we use ``docutils`` to parse and 15 | compile reStructuredText to HTML. You can write your posts in reST fashion: 16 | 17 | .. code-block:: rst 18 | 19 | Title 20 | ##### 21 | 22 | :type: page 23 | :tags: one, two 24 | 25 | Here begins the body ... 26 | 27 | Note, that reST's metadata markup for lists is different to YAML and will throw 28 | errors if not used correctly. Wrong reST markup will generally throw a lot of 29 | warnings and errors in the generated output. 30 | 31 | The reStructuredText filter has no further default settings beside 32 | ``initial_header_level`` set to 1. That means, the first-level heading uses 33 | ``

``, the second-level heading ``

`` and so on. ``

`` is reserved 34 | for the post's title. 35 | 36 | Usage 37 | ^^^^^ 38 | 39 | ============ ================================================== 40 | Requires ``docutils`` (or ``python-docutils``), optional 41 | ``pygments`` for syntax highlighting 42 | Aliases rst, rest, reST, restructuredtext 43 | Conflicts HTML, Markdown, Pandoc 44 | ============ ================================================== 45 | 46 | 47 | Extensions 48 | ^^^^^^^^^^ 49 | 50 | * .. autosimple:: acrylamid.filters.rstx_gist.Gist 51 | * .. autosimple:: acrylamid.filters.rstx_highlight.Highlight 52 | * .. autosimple:: acrylamid.filters.rstx_youtube.YouTube 53 | * .. autosimple:: acrylamid.filters.rstx_vimeo.Vimeo 54 | 55 | Limitations 56 | ^^^^^^^^^^^ 57 | 58 | Unlike Markdown or Textile, you can not write plain HTML in reST. The only 59 | way to include raw HTML is to use the ``.. raw:: html`` directive. This also 60 | means, that you can **not** use preprocessors that generate HTML such as ``liquid``. 61 | 62 | Tips 63 | ---- 64 | 65 | * To write `inline math`_ with a subset of LaTeX math syntax, so there is no 66 | need for additional extension like in Markdown, just use the ``math`` directive. 67 | 68 | * For highlighting source code, you can use the new `code directive`_ from 69 | docutils. For convenience (and backwards compatibility), ``code-block``, 70 | ``sourcecode`` and ``pygments`` map to the ``code`` directive as well. To 71 | generate a style sheet, you use ``pygmentize``: 72 | 73 | :: 74 | 75 | $ pygmentize -S trac -f html > pygments.css 76 | 77 | To get a list of all available styles use the interactive python interpreter. 78 | 79 | :: 80 | 81 | >>> from pygments import styles 82 | >>> print list(styles.get_all_styles()) 83 | 84 | .. _inline math: http://docutils.sourceforge.net/docs/ref/rst/directives.html#math 85 | .. _code directive: http://docutils.sourceforge.net/docs/ref/rst/directives.html#code 86 | -------------------------------------------------------------------------------- /acrylamid/filters/mdx_gist.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Markdown Gist, similar to rstx_gist.py 4 | # 5 | 6 | import re 7 | import markdown 8 | 9 | from acrylamid.lib.requests import get, HTTPError, URLError 10 | from acrylamid import log 11 | 12 | match = ['gist', 'gistraw'] 13 | 14 | GIST_RE = r'\[gist:\s*(?P\d+)(?:\s*(?P.+?))?\]' 15 | GISTRAW_RE = r'\[gistraw:\s*(?P\d+)(?:\s*(?P.+?))?\]' 16 | 17 | class GistExtension(markdown.Extension): 18 | """Add [gist:] and [gistraw:] to Pyton-Markdown Extensions""" 19 | 20 | def extendMarkdown(self, md, md_globals): 21 | self.md = md 22 | md.inlinePatterns.add('gist', GistPattern(GIST_RE), '_begin') 23 | md.inlinePatterns.add('gistraw', GistPattern(GISTRAW_RE), '_begin') 24 | 25 | class GistPattern(markdown.inlinepatterns.Pattern): 26 | """Replace [gist: id filename] with embedded Gist script. Filename is optional 27 | [gistraw: id filename] will return the raw text wrapped in a
 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('' % 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.xmlMark van Lent's weblogPracticing software developmenthttp://www.vlent.nl/static/images/favicon.icohttp://www.vlent.nl/static/images/favicon.ico2012-08-09T14:05:28ZMark van LentCreative Commons Attribution 3.0 Unported Licensetag:www.vlent.nl,2012-08-09:/weblog/2012/08/09/attributeerror-querymethodid-when-creating-object/"AttributeError: queryMethodId" when creating an object2012-08-09T14:05:28ZMark 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.&nbsp;</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: &lt;PathExpr standard:u'object|here'&gt; 14 | - Names: 15 | {'container': &lt;PloneSite at /site&gt;, 16 | ... 17 | 'user': &lt;PropertiedUser 'admin'&gt;} 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&nbsp;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)&hellip;</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('' % 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 |         
81 | 82 |
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 | --------------------------------------------------------------------------------