5 | {{ post.image|markdown|typogrify }}
6 | {{ post.excerpt|markdown|typogrify }}
7 |
10 |
11 | {% endmacro %}
12 |
13 | {% macro render_nav(menu, cls=None) -%}
14 | {% if menu -%}
15 |
39 | {%- endif %}
40 | {%- endmacro %}
--------------------------------------------------------------------------------
/hyde/layouts/starter/content/advanced/grouper.html:
--------------------------------------------------------------------------------
1 | ---
2 | index: 3
3 | title: Grouping
4 | tags:
5 | - sort
6 | - group
7 | learning_order: 2
8 | ---
9 |
10 | Grouping
11 | ========
12 |
13 | Content is very often grouped by theme, size, location or any other
14 | conceivable measure. Groups can be traversed in a few ways in Hyde, and
15 | [sorted](sorter.html) at the same time. Here are two common ways:
16 |
17 | Walking all groups and subgroups
18 | --------------------------------
19 |
20 | {# Resources are sorted by defining a sorter in the configuration file. #}
21 |
22 | {% for grp, res_walker in site.content.walk_level_groups() %}
23 | *{{ grp.description }}*
24 |
25 | {% for res in res_walker %}
26 | * [{{ res.slug|capitalize|replace("-"," ") }}]({{ res.full_url }})
27 | ({{ res.name }})
28 | {% endfor %}
29 |
30 | {% endfor %}
31 |
32 | {# The above code layout is not arbitrary. Remember that we're building
33 | a Markdown page so every space or line ending has a purpose.
34 | #}
35 |
36 | Listing only the specific (sub)group
37 | ------------------------------------
38 |
39 | {% for res in site.content.walk_resources_grouped_by_advanced() %}
40 | * [{{ res.slug|capitalize|replace("-"," ") }}]({{ res.full_url }})
41 | ({{ res.name }})
42 | {% endfor %}
43 |
44 | {# You can also call the top level group "level" to get all resources that
45 | are in the group. Or you can list all resources of the same group in the
46 | current node with "resource.node.walk_resource_grouped_by_advanced()".
47 | #}
48 |
49 | {{ macros.render_bottom_article_nav() }}
50 |
--------------------------------------------------------------------------------
/hyde/layouts/starter/content/advanced/tagger.html:
--------------------------------------------------------------------------------
1 | ---
2 | index: 4
3 | title: Tagging
4 | tags:
5 | - sort
6 | - tag
7 | learning_order: 3
8 | ---
9 |
10 | Tagging
11 | =======
12 |
13 | It seems that human beings want to tag everything. You can do it with
14 | Hyde also. In this example **tags** are used to represent technologies
15 | used to build a particular advanced page. So you can see that the
16 | **sorting** was needed for all advanced topics, but **grouping** was
17 | used only for overview and grouping pages.
18 |
19 | Listing by tags
20 | ---------------
21 |
22 | {# You can grab the list of all tags ... #}
23 | {% for tag, meta in site.tagger.tags %}
24 | *{{ tag }}*
25 |
26 | {# ... and get all resurces tagged with that node. #}
27 | {% for res in resource.node.walk_resources_tagged_with(tag) %}
28 | * [{{ res.slug|capitalize|replace("-"," ") }}]({{ res.full_url }})
29 | ({{ res.name }})
30 | {% endfor %}
31 |
32 | {% endfor %}
33 |
34 | {# Another way to walk through resources tagged with a specific tag is
35 | to use a method that contains that tag's name.
36 |
37 | {% for res in resource.node.walk_resources_tagged_with_sort() %}
38 | {% endfor %}
39 | #}
40 |
41 | Tag combination
42 | ---------------
43 |
44 | You can also search for combination of tags. If you search for a
45 | resource that has **sort**, **tag** and **group** tags, only an
46 | {% for res in resource.node.walk_resources_tagged_with('sort+tag+group') -%}
47 | [{{ res.slug }}]({{ res.full_url }})
48 | {%- endfor %}
49 | will be returned.
50 |
51 | {{ macros.render_bottom_article_nav() }}
52 |
--------------------------------------------------------------------------------
/hyde/layouts/starter/layout/macros.j2:
--------------------------------------------------------------------------------
1 | {# Generates the main and basic menu from context data in the site's
2 | configuration file.
3 | #}
4 | {% macro render_basic_menu() -%}
5 |
11 | {%- endmacro %}
12 |
13 |
14 | {# Generates the advanced menu from all files located in the content/advanced
15 | folder. Only advanced section files have 'index' metadata, so we can be
16 | certain that no other files will creep in.
17 | #}
18 | {% macro render_advanced_menu() -%}
19 |
20 | {% for res in site.content.walk_resources_sorted_by_index() %}
21 |
45 | {% endmacro %}
46 |
--------------------------------------------------------------------------------
/hyde/util.py:
--------------------------------------------------------------------------------
1 | """
2 | Module for python 2.6 compatibility.
3 | """
4 | import os
5 | from functools import partial
6 | from itertools import izip, tee
7 |
8 |
9 | def make_method(method_name, method_):
10 | def method__(*args, **kwargs):
11 | return method_(*args, **kwargs)
12 | method__.__name__ = method_name
13 | return method__
14 |
15 |
16 | def add_property(obj, method_name, method_, *args, **kwargs):
17 | m = make_method(method_name, partial(method_, *args, **kwargs))
18 | setattr(obj, method_name, property(m))
19 |
20 |
21 | def add_method(obj, method_name, method_, *args, **kwargs):
22 | m = make_method(method_name, partial(method_, *args, **kwargs))
23 | setattr(obj, method_name, m)
24 |
25 |
26 | def pairwalk(iterable):
27 | a, b = tee(iterable)
28 | next(b, None)
29 | return izip(a, b)
30 |
31 |
32 | def first_match(predicate, iterable):
33 | """
34 | Gets the first element matched by the predicate
35 | in the iterable.
36 | """
37 | for item in iterable:
38 | if predicate(item):
39 | return item
40 | return None
41 |
42 |
43 | def discover_executable(name, sitepath):
44 | """
45 | Finds an executable in the given sitepath or in the
46 | path list provided by the PATH environment variable.
47 | """
48 |
49 | # Check if an executable can be found in the site path first.
50 | # If not check the os $PATH for its presence.
51 |
52 | paths = [unicode(sitepath)] + os.environ['PATH'].split(os.pathsep)
53 | for path in paths:
54 | full_name = os.path.join(path, name)
55 | if os.path.exists(full_name):
56 | return full_name
57 | return None
--------------------------------------------------------------------------------
/hyde/layouts/starter/content/advanced/overview.html:
--------------------------------------------------------------------------------
1 | ---
2 | index: 1
3 | title: Advanced topics
4 | tags:
5 | - sort
6 | - group
7 | - tag
8 | learning_order: 4
9 | ---
10 |
11 | More advanced topics
12 | ====================
13 |
14 | If you have read and understood all basic topics covered in
15 | {% for res in site.content.walk_resources_grouped_by_basic()|reverse %}
16 | [{{ res.slug|capitalize|replace("-"," ") }}]({{ res.full_url }})
17 | {% endfor %}
18 | then you are ready for some more advanced features. They are explained in
19 | the same way as the basic part, building on the knowledge of the previous,
20 | so it is recommended that you follow them in the listed order.
21 |
22 |
23 | {# List all resources from a group, sort them by index and list their tags.
24 |
25 | Sometimes you'll have to add HTML to a Markdown file for styling
26 | or adding some special features, and Markdown is OK with that.
27 | #}
28 |
29 | {% for res in resource.node.walk_resources_sorted_by_learning_order() %}
30 | {#
31 | res.slug|capitalize|replace("-"," ") is just an example of how different
32 | commands can be chained together, but many times it will be much easier
33 | just to use meta data if a resource has it, like here -> res.meta.title
34 | #}
35 |
{{ res.meta.title }}
36 | {% if res.name == "overview.html" %}(this file) {% endif %}
37 |
38 | tags:
39 | {% for tag in res.meta.tags %}
40 | {#
41 | After wring the tag name, check if that is the last tag in the list. If
42 | it is, don't append the comma at the end.
43 | #}
44 | {{ tag }}{% if tag != res.meta.tags[-1] %},{% endif %}
45 | {% endfor %}
46 |
47 |
43 | {% endblock content%}
44 |
45 | {% block js %}
46 |
47 | {% block jquery %}
48 |
49 |
50 | {% endblock jquery %}
51 |
52 | {% block scripts %}{% endblock scripts %}
53 | {%endblock js %}
54 |
55 |
56 |
57 | {% endblock all %}
--------------------------------------------------------------------------------
/hyde/tests/ext/test_auto_extend.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Use nose
4 | `$ pip install nose`
5 | `$ nosetests`
6 | """
7 | from hyde.generator import Generator
8 | from hyde.site import Site
9 |
10 |
11 | from fswrap import File
12 | from nose.tools import nottest
13 | from pyquery import PyQuery
14 |
15 | TEST_SITE = File(__file__).parent.parent.child_folder('_test')
16 |
17 |
18 | class TestAutoExtend(object):
19 |
20 | def setUp(self):
21 | TEST_SITE.make()
22 | TEST_SITE.parent.child_folder(
23 | 'sites/test_jinja').copy_contents_to(TEST_SITE)
24 |
25 | def tearDown(self):
26 | TEST_SITE.delete()
27 |
28 | @nottest
29 | def assert_extended(self, s, txt, templ):
30 | content = (templ.strip() % txt).strip()
31 | bd = File(TEST_SITE.child('content/auto_extend.html'))
32 | bd.write(content)
33 | gen = Generator(s)
34 | gen.generate_resource_at_path(bd.path)
35 | res = s.content.resource_from_path(bd.path)
36 | target = File(s.config.deploy_root_path.child(res.relative_deploy_path))
37 | assert target.exists
38 | text = target.read_all()
39 | q = PyQuery(text)
40 | assert q('title').text().strip() == txt.strip()
41 |
42 | def test_can_auto_extend(self):
43 | s = Site(TEST_SITE)
44 | s.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin',
45 | 'hyde.ext.plugins.meta.AutoExtendPlugin',
46 | 'hyde.ext.plugins.text.BlockdownPlugin']
47 | txt ="This template tests to make sure blocks can be replaced with markdownish syntax."
48 | templ = """
49 | ---
50 | extends: base.html
51 | ---
52 | =====title========
53 | %s
54 | ====/title========"""
55 | self.assert_extended(s, txt, templ)
56 |
57 |
58 |
59 | def test_can_auto_extend_with_default_blocks(self):
60 | s = Site(TEST_SITE)
61 | s.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin',
62 | 'hyde.ext.plugins.meta.AutoExtendPlugin',
63 | 'hyde.ext.plugins.text.BlockdownPlugin']
64 | txt ="This template tests to make sure blocks can be replaced with markdownish syntax."
65 | templ = """
66 | ---
67 | extends: base.html
68 | default_block: title
69 | ---
70 | %s
71 | """
72 | self.assert_extended(s, txt, templ)
73 |
--------------------------------------------------------------------------------
/hyde/tests/ext/test_stylus.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Use nose
4 | `$ pip install nose`
5 | `$ nosetests`
6 | """
7 | from hyde.model import Expando
8 | from hyde.generator import Generator
9 | from hyde.site import Site
10 |
11 | from fswrap import File, Folder
12 |
13 | STYLUS_SOURCE = File(__file__).parent.child_folder('stylus')
14 | TEST_SITE = File(__file__).parent.parent.child_folder('_test')
15 |
16 | class TestStylus(object):
17 |
18 | def setUp(self):
19 | TEST_SITE.make()
20 | TEST_SITE.parent.child_folder(
21 | 'sites/test_jinja').copy_contents_to(TEST_SITE)
22 | STYLUS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/css'))
23 | File(TEST_SITE.child('content/media/css/site.css')).delete()
24 |
25 |
26 | def tearDown(self):
27 | TEST_SITE.delete()
28 |
29 | def test_can_execute_stylus(self):
30 | s = Site(TEST_SITE)
31 | s.config.plugins = ['hyde.ext.plugins.css.StylusPlugin']
32 | paths = ['/usr/local/share/npm/bin/stylus']
33 | for path in paths:
34 | if File(path).exists:
35 | s.config.stylus = Expando(dict(app=path))
36 | source = TEST_SITE.child('content/media/css/site.styl')
37 | target = File(Folder(s.config.deploy_root_path).child('media/css/site.css'))
38 | gen = Generator(s)
39 | gen.generate_resource_at_path(source)
40 |
41 | assert target.exists
42 | text = target.read_all()
43 | expected_text = File(STYLUS_SOURCE.child('expected-site.css')).read_all()
44 | assert text.strip() == expected_text.strip()
45 |
46 | def test_can_compress_with_stylus(self):
47 | s = Site(TEST_SITE)
48 | s.config.mode = "production"
49 | s.config.plugins = ['hyde.ext.plugins.css.StylusPlugin']
50 | paths = ['/usr/local/share/npm/bin/stylus']
51 | for path in paths:
52 | if File(path).exists:
53 | s.config.stylus = Expando(dict(app=path))
54 | source = TEST_SITE.child('content/media/css/site.styl')
55 | target = File(Folder(s.config.deploy_root_path).child('media/css/site.css'))
56 | gen = Generator(s)
57 | gen.generate_resource_at_path(source)
58 |
59 | assert target.exists
60 | text = target.read_all()
61 | expected_text = File(STYLUS_SOURCE.child('expected-site-compressed.css')).read_all()
62 | assert text.strip() == expected_text.strip()
63 |
--------------------------------------------------------------------------------
/hyde/ext/plugins/urls.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Contains classes and utilities related to hyde urls.
4 | """
5 | from hyde.plugin import Plugin
6 | from hyde.site import Site
7 |
8 | from functools import wraps
9 | from fswrap import File
10 |
11 | class UrlCleanerPlugin(Plugin):
12 | """
13 | Url Cleaner plugin for hyde. Adds to hyde the ability to generate clean
14 | urls.
15 |
16 | Configuration example
17 | ---------------------
18 | #yaml
19 | urlcleaner:
20 | index_file_names:
21 | # Identifies the files that represents a directory listing.
22 | # These file names are automatically stripped away when
23 | # the content_url function is called.
24 | - index.html
25 | strip_extensions:
26 | # The following extensions are automatically removed when
27 | # generating the urls using content_url function.
28 | - html
29 | # This option will append a slash to the end of directory paths
30 | append_slash: true
31 | """
32 |
33 | def __init__(self, site):
34 | super(UrlCleanerPlugin, self).__init__(site)
35 |
36 | def begin_site(self):
37 | """
38 | Replace the content_url method in the site object with a custom method
39 | that cleans urls based on the given configuration.
40 | """
41 | config = self.site.config
42 |
43 | if not hasattr(config, 'urlcleaner'):
44 | return
45 |
46 | if (hasattr(Site, '___url_cleaner_patched___')):
47 | return
48 |
49 | settings = config.urlcleaner
50 |
51 | def clean_url(urlgetter):
52 | @wraps(urlgetter)
53 | def wrapper(site, path, safe=None):
54 | url = urlgetter(site, path, safe)
55 | index_file_names = getattr(settings,
56 | 'index_file_names',
57 | ['index.html'])
58 | rep = File(url)
59 | if rep.name in index_file_names:
60 | url = rep.parent.path.rstrip('/')
61 | if hasattr(settings, 'append_slash') and \
62 | settings.append_slash:
63 | url += '/'
64 | elif hasattr(settings, 'strip_extensions'):
65 | if rep.kind in settings.strip_extensions:
66 | url = rep.parent.child(rep.name_without_extension)
67 | return url or '/'
68 | return wrapper
69 |
70 | Site.___url_cleaner_patched___ = True
71 | Site.content_url = clean_url(Site.content_url)
72 |
--------------------------------------------------------------------------------
/hyde/ext/plugins/languages.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Contains classes to help manage multi-language pages.
4 | """
5 |
6 | from hyde.plugin import Plugin
7 |
8 | class LanguagePlugin(Plugin):
9 | """
10 | Each page should be tagged with a language using `language` meta
11 | data. Each page should also have an UUID stored in `uuid` meta
12 | data. Pages with different languages but the same UUID will be
13 | considered a translation of each other.
14 |
15 | For example, consider that you have the following content tree:
16 | - `en/`
17 | - `fr/`
18 |
19 | In `en/meta.yaml`, you set `language: en` and in `fr/meta.yaml`, you
20 | set `language: fr`. In `en/my-first-page.html`, you put something like
21 | this::
22 | -------
23 | uuid: page1
24 | -------
25 | My first page
26 |
27 | And in `fr/ma-premiere-page.html`, you put something like this::
28 | -------
29 | uuid: page1
30 | -------
31 | Ma première page
32 |
33 | You'll get a `translations` attribute attached to the resource
34 | with the list of resources that are a translation of this one.
35 | """
36 |
37 | def __init__(self, site):
38 | super(LanguagePlugin, self).__init__(site)
39 | self.languages = {} # Associate a UUID to the list of resources available
40 |
41 | def begin_site(self):
42 | """
43 | Initialize plugin. Build the language tree for each node
44 | """
45 | # Build association between UUID and list of resources
46 | for node in self.site.content.walk():
47 | for resource in node.resources:
48 | try:
49 | uuid = resource.meta.uuid
50 | language = resource.meta.language
51 | except AttributeError:
52 | continue
53 | if uuid not in self.languages:
54 | self.languages[uuid] = []
55 | self.languages[uuid].append(resource)
56 | # Add back the information about other languages
57 | for uuid, resources in self.languages.items():
58 | for resource in resources:
59 | language = resource.meta.language
60 | resource.translations = \
61 | [r for r in resources
62 | if r.meta.language != language]
63 | translations = ",".join([t.meta.language for t in resource.translations])
64 | self.logger.debug(
65 | "Adding translations for resource [%s] from %s to %s" % (resource,
66 | language,
67 | translations))
68 |
--------------------------------------------------------------------------------
/hyde/lib/pygments/rst_directive.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | The Pygments reStructuredText directive
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 |
6 | This fragment is a Docutils_ 0.5 directive that renders source code
7 | (to HTML only, currently) via Pygments.
8 |
9 | To use it, adjust the options below and copy the code into a module
10 | that you import on initialization. The code then automatically
11 | registers a ``sourcecode`` directive that you can use instead of
12 | normal code blocks like this::
13 |
14 | .. sourcecode:: python
15 |
16 | My code goes here.
17 |
18 | If you want to have different code styles, e.g. one with line numbers
19 | and one without, add formatters with their names in the VARIANTS dict
20 | below. You can invoke them instead of the DEFAULT one by using a
21 | directive option::
22 |
23 | .. sourcecode:: python
24 | :linenos:
25 |
26 | My code goes here.
27 |
28 | Look at the `directive documentation`_ to get all the gory details.
29 |
30 | .. _Docutils: http://docutils.sf.net/
31 | .. _directive documentation:
32 | http://docutils.sourceforge.net/docs/howto/rst-directives.html
33 |
34 | :copyright: Copyright 2006-2011 by the Pygments team, see AUTHORS.
35 | :license: BSD, see LICENSE for details.
36 | """
37 |
38 | # Options
39 | # ~~~~~~~
40 |
41 | # Set to True if you want inline CSS styles instead of classes
42 | INLINESTYLES = False
43 |
44 | from pygments.formatters import HtmlFormatter
45 |
46 | # The default formatter
47 | DEFAULT = HtmlFormatter(noclasses=INLINESTYLES)
48 |
49 | # Add name -> formatter pairs for every variant you want to use
50 | VARIANTS = {
51 | 'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True),
52 | }
53 |
54 |
55 | from docutils import nodes
56 | from docutils.parsers.rst import directives, Directive
57 |
58 | from pygments import highlight
59 | from pygments.lexers import get_lexer_by_name, TextLexer
60 |
61 | class Pygments(Directive):
62 | """ Source code syntax hightlighting.
63 | """
64 | required_arguments = 1
65 | optional_arguments = 0
66 | final_argument_whitespace = True
67 | option_spec = dict([(key, directives.flag) for key in VARIANTS])
68 | has_content = True
69 |
70 | def run(self):
71 | self.assert_has_content()
72 | try:
73 | lexer = get_lexer_by_name(self.arguments[0])
74 | except ValueError:
75 | # no lexer found - use the text one instead of an exception
76 | lexer = TextLexer()
77 | # take an arbitrary option if more than one is given
78 | formatter = self.options and VARIANTS[self.options.keys()[0]] or DEFAULT
79 | parsed = highlight(u'\n'.join(self.content), lexer, formatter)
80 | return [nodes.raw('', parsed, format='html')]
81 |
82 | directives.register_directive('sourcecode', Pygments)
83 |
--------------------------------------------------------------------------------
/hyde/tests/ext/test_uglify.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Use nose
4 | `$ pip install nose`
5 | `$ nosetests`
6 | """
7 | from hyde.model import Expando
8 | from hyde.generator import Generator
9 | from hyde.site import Site
10 |
11 | from fswrap import File, Folder
12 | from hyde.tests.util import assert_no_diff
13 |
14 | UGLIFY_SOURCE = File(__file__).parent.child_folder('uglify')
15 | TEST_SITE = File(__file__).parent.parent.child_folder('_test')
16 |
17 |
18 | class TestUglify(object):
19 |
20 | def setUp(self):
21 | TEST_SITE.make()
22 | TEST_SITE.parent.child_folder(
23 | 'sites/test_jinja').copy_contents_to(TEST_SITE)
24 | JS = TEST_SITE.child_folder('content/media/js')
25 | JS.make()
26 | UGLIFY_SOURCE.copy_contents_to(JS)
27 |
28 |
29 | def tearDown(self):
30 | TEST_SITE.delete()
31 |
32 | def test_can_uglify(self):
33 | s = Site(TEST_SITE)
34 | s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin']
35 | s.config.mode = "production"
36 | source = TEST_SITE.child('content/media/js/jquery.js')
37 | target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
38 | gen = Generator(s)
39 | gen.generate_resource_at_path(source)
40 |
41 | assert target.exists
42 | expected = UGLIFY_SOURCE.child_file('expected-jquery.js').read_all()
43 | # TODO: Very fragile. Better comparison needed.
44 | text = target.read_all()
45 | assert_no_diff(expected, text)
46 |
47 | def test_uglify_with_extra_options(self):
48 | s = Site(TEST_SITE)
49 | s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin']
50 | s.config.mode = "production"
51 | s.config.uglify = Expando(dict(args={"comments":"/http\:\/\/jquery.org\/license/"}))
52 | source = TEST_SITE.child('content/media/js/jquery.js')
53 | target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
54 | gen = Generator(s)
55 | gen.generate_resource_at_path(source)
56 |
57 | assert target.exists
58 | expected = UGLIFY_SOURCE.child_file('expected-jquery-nc.js').read_all()
59 | # TODO: Very fragile. Better comparison needed.
60 | text = target.read_all()
61 | assert_no_diff(expected, text)
62 |
63 | def test_no_uglify_in_dev_mode(self):
64 | s = Site(TEST_SITE)
65 | s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin']
66 | s.config.mode = "dev"
67 | source = TEST_SITE.child('content/media/js/jquery.js')
68 | target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
69 | gen = Generator(s)
70 | gen.generate_resource_at_path(source)
71 |
72 | assert target.exists
73 | expected = UGLIFY_SOURCE.child_file('jquery.js').read_all()
74 | # TODO: Very fragile. Better comparison needed.
75 | text = target.read_all()
76 | assert_no_diff(expected, text)
77 |
78 |
79 |
--------------------------------------------------------------------------------
/hyde/layouts/basic/content/media/css/syntax.css:
--------------------------------------------------------------------------------
1 | .c, .cm { color: #998; font-style: italic } /* Comments */
2 | .err { color: #a61717; background-color: #e3d2d2 } /* Error */
3 | .k { font-weight: bold } /* Keyword */
4 | .o { font-weight: bold } /* Operator */
5 | .cp { color: #999; font-weight: bold } /* Comment.Preproc */
6 | .c1 { color: #998; font-style: italic } /* Comment.Single */
7 | .cs { color: #999; font-weight: bold; font-style: italic } /* Comment.Special */
8 | .gd { color: #000; background-color: #ffdddd } /* Generic.Deleted */
9 | .gd .x { color: #000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
10 | .ge { font-style: italic } /* Generic.Emph */
11 | .gr { color: #a00 } /* Generic.Error */
12 | .gh { color: #999 } /* Generic.Heading */
13 | .gi { color: #000; background-color: #ddffdd } /* Generic.Inserted */
14 | .gi .x { color: #000; background-color: #aaffaa } /* Generic.Inserted.Specific */
15 | .go { color: #888 } /* Generic.Output */
16 | .gp { color: #555 } /* Generic.Prompt */
17 | .gs { font-weight: bold } /* Generic.Strong */
18 | .gu { color: #aaaaaa } /* Generic.Subheading */
19 | .gt { color: #a00 } /* Generic.Traceback */
20 | .kc { font-weight: bold } /* Keyword.Constant */
21 | .kd { font-weight: bold } /* Keyword.Declaration */
22 | .kp { font-weight: bold } /* Keyword.Pseudo */
23 | .kr { font-weight: bold } /* Keyword.Reserved */
24 | .kt { color: #445588; font-weight: bold } /* Keyword.Type */
25 | .m { color: #099 } /* Literal.Number */
26 | .s { color: #d14 } /* Literal.String */
27 | .na { color: #008080 } /* Name.Attribute */
28 | .nb { color: #0086B3 } /* Name.Builtin */
29 | .nc { color: #445588; font-weight: bold } /* Name.Class */
30 | .no { color: #008080 } /* Name.Constant */
31 | .ni { color: #800080 } /* Name.Entity */
32 | .ne { color: #900; font-weight: bold } /* Name.Exception */
33 | .nf { color: #900; font-weight: bold } /* Name.Function */
34 | .nn { color: #555 } /* Name.Namespace */
35 | .nt { color: #000080 } /* Name.Tag */
36 | .nv { color: #008080 } /* Name.Variable */
37 | .ow { font-weight: bold } /* Operator.Word */
38 | .w { color: #bbb } /* Text.Whitespace */
39 | .mf { color: #099 } /* Literal.Number.Float */
40 | .mh { color: #099 } /* Literal.Number.Hex */
41 | .mi { color: #099 } /* Literal.Number.Integer */
42 | .mo { color: #099 } /* Literal.Number.Oct */
43 | .sb { color: #d14 } /* Literal.String.Backtick */
44 | .sc { color: #d14 } /* Literal.String.Char */
45 | .sd { color: #d14 } /* Literal.String.Doc */
46 | .s2 { color: #d14 } /* Literal.String.Double */
47 | .se { color: #d14 } /* Literal.String.Escape */
48 | .sh { color: #d14 } /* Literal.String.Heredoc */
49 | .si { color: #d14 } /* Literal.String.Interpol */
50 | .sx { color: #d14 } /* Literal.String.Other */
51 | .sr { color: #009926 } /* Literal.String.Regex */
52 | .s1 { color: #d14 } /* Literal.String.Single */
53 | .ss { color: #990073 } /* Literal.String.Symbol */
54 | .bp { color: #999 } /* Name.Builtin.Pseudo */
55 | .vc { color: #008080 } /* Name.Variable.Class */
56 | .vg { color: #008080 } /* Name.Variable.Global */
57 | .vi { color: #008080 } /* Name.Variable.Instance */
58 | .il { color: #099 } /* Literal.Number.Integer.Long */
59 |
60 |
--------------------------------------------------------------------------------
/hyde/tests/ext/test_markings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Use nose
4 | `$ pip install nose`
5 | `$ nosetests`
6 | """
7 | from hyde.generator import Generator
8 | from hyde.site import Site
9 |
10 | from fswrap import File
11 | from pyquery import PyQuery
12 |
13 | TEST_SITE = File(__file__).parent.parent.child_folder('_test')
14 |
15 | def assert_valid_conversion(html):
16 | assert html
17 | q = PyQuery(html)
18 | assert "is_processable" not in html
19 | assert q("h1")
20 | assert "This is a" in q("h1").text()
21 | assert "heading" in q("h1").text()
22 | assert q(".amp").length == 1
23 | assert "mark" not in html
24 | assert "reference" not in html
25 | assert '.' not in q.text()
26 | assert '/' not in q.text()
27 |
28 | class TestMarkings(object):
29 |
30 | def setUp(self):
31 | TEST_SITE.make()
32 | TEST_SITE.parent.child_folder(
33 | 'sites/test_jinja').copy_contents_to(TEST_SITE)
34 |
35 | def tearDown(self):
36 | TEST_SITE.delete()
37 |
38 |
39 |
40 | def test_mark(self):
41 | text = u"""
42 | ===
43 | is_processable: False
44 | ===
45 | {% filter markdown|typogrify %}
46 | §§ heading
47 | This is a heading
48 | =================
49 | §§ /heading
50 |
51 | §§ content
52 | Hyde & Jinja
53 | §§ /
54 |
55 | {% endfilter %}
56 | """
57 |
58 | text2 = """
59 | {% refer to "inc.md" as inc %}
60 | {% filter markdown|typogrify %}
61 | {{ inc.heading }}
62 | {{ inc.content }}
63 | {% endfilter %}
64 | """
65 | site = Site(TEST_SITE)
66 | site.config.plugins = [
67 | 'hyde.ext.plugins.meta.MetaPlugin',
68 | 'hyde.ext.plugins.text.MarkingsPlugin']
69 | inc = File(TEST_SITE.child('content/inc.md'))
70 | inc.write(text)
71 | site.load()
72 | gen = Generator(site)
73 | gen.load_template_if_needed()
74 |
75 | template = gen.template
76 | html = template.render(text2, {}).strip()
77 | assert_valid_conversion(html)
78 |
79 | def test_reference(self):
80 | text = u"""
81 | ===
82 | is_processable: False
83 | ===
84 | {% filter markdown|typogrify %}
85 | §§ heading
86 | This is a heading
87 | =================
88 | §§ /heading
89 |
90 | §§ content
91 | Hyde & Jinja
92 | §§ /
93 |
94 | {% endfilter %}
95 | """
96 |
97 | text2 = u"""
98 | ※ inc.md as inc
99 | {% filter markdown|typogrify %}
100 | {{ inc.heading }}
101 | {{ inc.content }}
102 | {% endfilter %}
103 | """
104 | site = Site(TEST_SITE)
105 | site.config.plugins = [
106 | 'hyde.ext.plugins.meta.MetaPlugin',
107 | 'hyde.ext.plugins.text.MarkingsPlugin',
108 | 'hyde.ext.plugins.text.ReferencePlugin']
109 | inc = File(site.content.source_folder.child('inc.md'))
110 | inc.write(text.strip())
111 | src = File(site.content.source_folder.child('src.html'))
112 | src.write(text2.strip())
113 | gen = Generator(site)
114 | gen.generate_all()
115 | f = File(site.config.deploy_root_path.child(src.name))
116 | assert f.exists
117 | html = f.read_all()
118 | assert_valid_conversion(html)
119 |
--------------------------------------------------------------------------------
/hyde/layouts/starter/site.yaml:
--------------------------------------------------------------------------------
1 | mode: learning
2 | media_root: media
3 | media_url: /media
4 | base_url: /
5 | # If your site is nested inside of a bigger one, you can use media_url and
6 | # base_url to properly generate links on your site. For example, if your URL
7 | # will be some.domain.com/starter/, use:
8 | # media_url: /starter/media
9 | # base_url: /starter/
10 | template: hyde.ext.templates.jinja.Jinja2Template
11 | plugins:
12 | - hyde.ext.plugins.meta.MetaPlugin
13 | - hyde.ext.plugins.meta.AutoExtendPlugin
14 | # Plugins needed for the advances section.
15 | - hyde.ext.plugins.meta.SorterPlugin
16 | - hyde.ext.plugins.meta.GrouperPlugin
17 | - hyde.ext.plugins.meta.TaggerPlugin
18 | context:
19 | data:
20 | author:
21 | name: Merlin Rebrović
22 | url: "http://merlin.rebrovic.net"
23 | layout:
24 | name: Hyde Starter Kit
25 | url: "https://github.com/merlinrebrovic/hyde-starter-kit"
26 | project:
27 | name: Hyde
28 | url: "http://hyde.github.com"
29 | install: "https://github.com/hyde/hyde#installation"
30 | menu:
31 | - title: Home
32 | url: index.html
33 | - title: First steps
34 | url: first-steps.html
35 | - title: About
36 | url: about.html
37 |
38 | ### Advanced part ###
39 |
40 | # This defines meta data on the whole site.
41 | meta:
42 | # 'nodemeta' will tell Hyde what file to look for inside a folder from
43 | # which to apply meta data to all files (resources) inside it. This is
44 | # a great way of simply adding or modifying properties of a very large
45 | # number of files.
46 | nodemeta: meta.yaml
47 | ga_tracking_code: XX-XXXXXXXX-X
48 |
49 | sorter:
50 | name: # the name of the sorter (no pun intended)
51 | attr: name # by which attributes will resources be sorted
52 | filters:
53 | source_file.kind: html
54 | # You can include only files from a certain folder.
55 | #resource.node: (name of the folder)
56 | #reverse: True # if you need the list backwards
57 | file_type:
58 | attr:
59 | - source_file.kind
60 | - source_file.size
61 | index:
62 | attr: meta.index
63 | filters:
64 | source_file.kind: html
65 | learning_order:
66 | attr: meta.learning_order
67 | filters:
68 | source_file.kind: html
69 |
70 | grouper:
71 | level:
72 | sorter: name
73 | description: Difficulty levels
74 | groups:
75 | - name: basic
76 | description: Basic
77 | - name: advanced
78 | description: Advanced
79 | # You can have more than one group section, depending on your needs.
80 | # For example: "categories", "menu sections", etc.
81 | #category:
82 | # description: To which category a blog post belongs to.
83 | # groups:
84 | # - name: software
85 | # description: Software engineering
86 | # - name: web
87 | # description: Web technologies
88 | # - name: seo
89 | # description: Search Engine Optimization
90 |
91 | tagger:
92 | sorter: name
93 |
--------------------------------------------------------------------------------
/hyde/layouts/starter/content/media/css/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | html, body {
7 | color: #ddd;
8 | background-color: black;
9 | }
10 |
11 | body {
12 | margin: 0 auto;
13 | width: 980px;
14 | min-height: 700px;
15 | background: url('../img/background.jpg') top left no-repeat;
16 | position: relative;
17 | font-size: 16px;
18 | font-family: Tahoma, sans-serif;
19 | }
20 |
21 | strong {
22 | color: white;
23 | }
24 |
25 | a {
26 | color: #f1ee00;
27 | }
28 |
29 | a:hover, a:focus {
30 | text-decoration: none;
31 | }
32 |
33 | p, ul, ol {
34 | margin-bottom: 22px;
35 | }
36 |
37 | ul, ol {
38 | margin-left: 28px;
39 | }
40 |
41 | header {
42 | position: absolute;
43 | top: 120px;
44 | right: 120px;
45 | font-family: Anton, Tahoma, sans-serif;
46 | text-transform: uppercase;
47 | font-weight: none;
48 | }
49 |
50 | header h1 {
51 | font-size: 50px;
52 | font-weight: normal;
53 | text-shadow: -1px 2px 1px black;
54 | color: white;
55 | }
56 |
57 | header h1 span {
58 | color: #f1ee00;
59 | }
60 |
61 | header p {
62 | font-size: 28px;
63 | font-weight: normal;
64 | letter-spacing: 11px;
65 | text-shadow: -1px 2px 1px black;
66 | color: white;
67 | }
68 |
69 | nav {
70 | position: absolute;
71 | top: 340px;
72 | right: 120px;
73 | width: 250px;
74 | text-align: right;
75 | font-family: Anton, Tahoma, sans-serif;
76 | }
77 |
78 | nav a,
79 | nav a:visited {
80 | text-decoration: none;
81 | color: white;
82 | text-transform: uppercase;
83 | font-size: 28px;
84 | }
85 |
86 | nav a.selected {
87 | color: #888;
88 | }
89 |
90 | nav a:hover {
91 | color: #f1ee00;
92 | }
93 |
94 | nav ul li {
95 | list-style-type: none;
96 | }
97 |
98 | nav, aside {
99 | /* to line up with the header */
100 | padding-right: 5px;
101 | }
102 |
103 | #content {
104 | line-height: 1.5em;
105 | position: absolute;
106 | top: 350px;
107 | left: 180px;
108 | width: 410px;
109 | padding-bottom: 100px;
110 | }
111 |
112 | #content h1 {
113 | font-weight: normal;
114 | font-size: 28px;
115 | font-family: Anton, Tahoma, sans-serif;
116 | text-transform: uppercase;
117 | margin-bottom: 22px;
118 | color: white;
119 | }
120 |
121 | #content h2 {
122 | font-weight: bold;
123 | font-size: 18px;
124 | font-family: Tahoma, sans-serif;
125 | margin-bottom: 22px;
126 | border-bottom: 1px dashed #888;
127 | }
128 |
129 | #content code {
130 | display: block;
131 | font-family: monospace;
132 | font-size: 12px;
133 | background-color: #222;
134 | padding: 5px 10px;
135 | margin-bottom: 20px;
136 | border-top: 1px solid #444;
137 | border-right: 1px solid #333;
138 | border-bottom: 1px solid #222;
139 | border-left: 1px solid #111;
140 | }
141 |
142 | #content .tags {
143 | font-size: 14px;
144 | color: #888;
145 | }
146 |
147 | #content .bottom_article_nav {
148 | border-top: 1px dashed #888;
149 | overflow: hidden;
150 | margin-top: 40px;
151 | padding-top: 10px;
152 | }
153 |
154 | #content .bottom_article_nav .next {
155 | float: right;
156 | }
157 |
--------------------------------------------------------------------------------
/hyde/layouts/starter/content/first-steps.html:
--------------------------------------------------------------------------------
1 | ---
2 | extends: base.j2
3 | title: First steps
4 | ---
5 | {# In-file metadata. Supplements data from the site's configuration file
6 | and node (folder) level data in "meta.yaml" files.
7 |
8 | Use the AutoExtendPlugin to extend templates from metadata. In this
9 | case the metadata and "extends" statement has to be on the top of the
10 | file.
11 | #}
12 |
13 | {% block content %}
14 | Walk this way if you're a beginner
15 | ==================================
16 |
17 | This template was created to look at its code. So you should spend about
18 | 5% of your time looking at the web from the outside and the other 95%
19 | tinkering with things under the hood.
20 |
21 | The template is not perfect nor does it contain the best practices. It
22 | is not even consistent. It is designed to help you with the learning
23 | process. If you follow the steps below, you'll get a pretty good picture of
24 | how things work. Every step adds some new features and builds upon the
25 | previous one.
26 |
27 |
28 | 1. Site structure and configuration file
29 | ----------------------------------------
30 |
31 | The site is made of two folders and a [Hyde configuration][hyde_config]
32 | file. The folders are **content** and **layout**.
33 |
34 | **content** contains all your page content, blog articles, pictures and
35 | resources like CSS and JavaScript. Everything that is unique is here.
36 |
37 | **layout** contains templates and macros. Everything that you'll want to
38 | reuse is here.
39 |
40 |
41 | 2. Jinja2 template
42 | ------------------
43 |
44 | **base.j2** is a very short and simple Jinja2 template. This way you can
45 | concentrate on some of the basic features. Notice meta and context
46 | variables invocation inside curly braces, dynamic media path generation
47 | and running all content through the Markdown filter.
48 |
49 | **macros.j2** contains macros for common and repetitive tasks.
50 |
51 | For more information or to try something new, visit the extensive [Jinja2
52 | documentation][jinja2_docs].
53 |
54 |
55 | 3. Content
56 | ----------
57 |
58 | Look at the three files in this order: [index.html](index.html),
59 | [first-steps.html](first-steps.html) and [about.html](about.html).
60 |
61 | [Index](index.html) extends the base layout in the classic Jinja way and
62 | fills the content block with some simple Markdown data.
63 |
64 | [First steps](first-steps.html) goes a step furher. It introduces the
65 | in-file metadata that plugins will use to extend and fill the layout. It
66 | also uses some new Markdown features.
67 |
68 | [About](about.html) has a **default_block** metadata and mixes Markdown
69 | content with Jinja templates.
70 |
71 | [hyde_config]: http://hyde.github.com/config.html "Hyde configuration"
72 | [jinja2_docs]: http://jinja.pocoo.org/docs/templates/ "Jinja2 documentation"
73 |
74 |
75 | 4. Advanced sections
76 | --------------------
77 |
78 | While searching and navigating this template you'll find more files and
79 | sections than mentioned on this page (something like **meta.yaml**
80 | files, the **content/advanced** folder or other Jinja templates). Those
81 | files are needed for the [advanced topics](advanced/overview.html) so
82 | just ignore them at the beginning. They will start to make sense while
83 | you're working through the template or will be explicitly explained when
84 | the right time comes.
85 | {% endblock %}
86 |
--------------------------------------------------------------------------------
/hyde/ext/publishers/dvcs.py:
--------------------------------------------------------------------------------
1 | """
2 | Contains classes and utilities that help publishing a hyde website to
3 | distributed version control systems.
4 | """
5 |
6 | from hyde.publisher import Publisher
7 |
8 | import abc
9 | from subprocess import Popen, PIPE
10 |
11 | class DVCS(Publisher):
12 | __metaclass__ = abc.ABCMeta
13 |
14 | def initialize(self, settings):
15 | self.settings = settings
16 | self.path = self.site.sitepath.child_folder(settings.path)
17 | self.url = settings.url
18 | self.branch = getattr(settings, 'branch', 'master')
19 | self.switch(self.branch)
20 |
21 | @abc.abstractmethod
22 | def pull(self): pass
23 |
24 | @abc.abstractmethod
25 | def push(self): pass
26 |
27 | @abc.abstractmethod
28 | def commit(self, message): pass
29 |
30 | @abc.abstractmethod
31 | def switch(self, branch): pass
32 |
33 | @abc.abstractmethod
34 | def add(self, path="."): pass
35 |
36 | @abc.abstractmethod
37 | def merge(self, branch): pass
38 |
39 |
40 | def publish(self):
41 | super(DVCS, self).publish()
42 | if not self.path.exists:
43 | raise Exception("The destination repository must exist.")
44 | self.site.config.deploy_root_path.copy_contents_to(self.path)
45 | self.add()
46 | self.commit(self.message)
47 | self.push()
48 |
49 |
50 |
51 | class Git(DVCS):
52 | """
53 | Acts as a publisher to a git repository. Can be used to publish to
54 | github pages.
55 | """
56 |
57 | def add(self, path="."):
58 | cmd = Popen('git add "%s"' % path,
59 | cwd=unicode(self.path), stdout=PIPE, shell=True)
60 | cmdresult = cmd.communicate()[0]
61 | if cmd.returncode:
62 | raise Exception(cmdresult)
63 |
64 | def pull(self):
65 | self.switch(self.branch)
66 | cmd = Popen("git pull origin %s" % self.branch,
67 | cwd=unicode(self.path),
68 | stdout=PIPE,
69 | shell=True)
70 | cmdresult = cmd.communicate()[0]
71 | if cmd.returncode:
72 | raise Exception(cmdresult)
73 |
74 | def push(self):
75 | cmd = Popen("git push origin %s" % self.branch,
76 | cwd=unicode(self.path), stdout=PIPE,
77 | shell=True)
78 | cmdresult = cmd.communicate()[0]
79 | if cmd.returncode:
80 | raise Exception(cmdresult)
81 |
82 |
83 | def commit(self, message):
84 | cmd = Popen('git commit -a -m"%s"' % message,
85 | cwd=unicode(self.path), stdout=PIPE, shell=True)
86 | cmdresult = cmd.communicate()[0]
87 | if cmd.returncode:
88 | raise Exception(cmdresult)
89 |
90 | def switch(self, branch):
91 | self.branch = branch
92 | cmd = Popen('git checkout %s' % branch,
93 | cwd=unicode(self.path), stdout=PIPE, shell=True)
94 | cmdresult = cmd.communicate()[0]
95 | if cmd.returncode:
96 | raise Exception(cmdresult)
97 |
98 | def merge(self, branch):
99 | cmd = Popen('git merge %s' % branch,
100 | cwd=unicode(self.path), stdout=PIPE, shell=True)
101 | cmdresult = cmd.communicate()[0]
102 | if cmd.returncode:
103 | raise Exception(cmdresult)
--------------------------------------------------------------------------------
/hyde/tests/sites/test_sorter/content/blog/another-sad-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: Another Sad Post
3 | description: >
4 | Something else sad happened.
5 | created: !!timestamp '2011-03-01 10:00:00'
6 | index: 2
7 | ---
8 |
9 | {% mark excerpt -%}
10 |
11 | I went and dressed sadly. It will show you pretty well how pipped I was when I
12 | tell you that I near as a toucher put on a white tie with a dinner-jacket. I
13 | sallied out for a bit of food more to pass the time than because I wanted it.
14 | It seemed brutal to be wading into the bill of fare with poor old Bicky headed
15 | for the breadline.
16 |
17 | {%- endmark %}
18 |
19 | When I got back old Chiswick had gone to bed, but Bicky was there, hunched up
20 | in an arm-chair, brooding pretty tensely, with a cigarette hanging out of the
21 | corner of his mouth and a more or less glassy stare in his eyes. He had the
22 | aspect of one who had been soaked with what the newspaper chappies call "some
23 | blunt instrument."
24 |
25 | "This is a bit thick, old thing—what!" I said.
26 |
27 | He picked up his glass and drained it feverishly, overlooking the fact that it
28 | hadn't anything in it.
29 |
30 | "I'm done, Bertie!" he said.
31 |
32 | He had another go at the glass. It didn't seem to do him any good.
33 |
34 | "If only this had happened a week later, Bertie! My next month's money was due
35 | to roll in on Saturday. I could have worked a wheeze I've been reading about
36 | in the magazine advertisements. It seems that you can make a dashed amount of
37 | money if you can only collect a few dollars and start a chicken-farm. Jolly
38 | sound scheme, Bertie! Say you buy a hen—call it one hen for the sake of
39 | argument. It lays an egg every day of the week. You sell the eggs seven for
40 | twenty-five cents. Keep of hen costs nothing. Profit practically twenty-five
41 | cents on every seven eggs. Or look at it another way: Suppose you have a dozen
42 | eggs. Each of the hens has a dozen chickens. The chickens grow up and have
43 | more chickens. Why, in no time you'd have the place covered knee-deep in hens,
44 | all laying eggs, at twenty-five cents for every seven. You'd make a fortune.
45 | Jolly life, too, keeping hens!" He had begun to get quite worked up at the
46 | thought of it, but he slopped back in his chair at this juncture with a good
47 | deal of gloom. "But, of course, it's no good," he said, "because I haven't the
48 | cash."
49 |
50 | "You've only to say the word, you know, Bicky, old top."
51 |
52 | "Thanks awfully, Bertie, but I'm not going to sponge on you."
53 |
54 | That's always the way in this world. The chappies you'd like to lend money to
55 | won't let you, whereas the chappies you don't want to lend it to will do
56 | everything except actually stand you on your head and lift the specie out of
57 | your pockets. As a lad who has always rolled tolerably free in the right
58 | stuff, I've had lots of experience of the second class. Many's the time, back
59 | in London, I've hurried along Piccadilly and felt the hot breath of the
60 | toucher on the back of my neck and heard his sharp, excited yapping as he
61 | closed in on me. I've simply spent my life scattering largesse to blighters I
62 | didn't care a hang for; yet here was I now, dripping doubloons and pieces of
63 | eight and longing to hand them over, and Bicky, poor fish, absolutely on his
64 | uppers, not taking any at any price.
65 |
66 | "Well, there's only one hope, then."
67 |
68 | "What's that?"
69 |
70 | "Jeeves."
71 |
72 | "Sir?"
73 |
74 | There was Jeeves, standing behind me, full of zeal. In this matter of
75 | shimmering into rooms the chappie is rummy to a degree. You're sitting in the
76 | old armchair, thinking of this and that, and then suddenly you look up, and
77 | there he is. He moves from point to point with as little uproar as a jelly
78 | fish. The thing startled poor old Bicky considerably. He rose from his seat
79 | like a rocketing pheasant. I'm used to Jeeves now, but often in the days when
80 | he first came to me I've bitten my tongue freely on finding him unexpectedly
81 | in my midst.
82 |
83 | [My Man Jeeves by PG Wodehouse][MMJ]
84 |
85 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
86 |
--------------------------------------------------------------------------------
/hyde/tests/sites/test_paginator/content/blog/another-sad-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: Another Sad Post
3 | description: >
4 | Something else sad happened.
5 | created: !!timestamp '2011-03-01 10:00:00'
6 | tags:
7 | - sad
8 | - events
9 | ---
10 |
11 | {% mark excerpt -%}
12 |
13 | I went and dressed sadly. It will show you pretty well how pipped I was when I
14 | tell you that I near as a toucher put on a white tie with a dinner-jacket. I
15 | sallied out for a bit of food more to pass the time than because I wanted it.
16 | It seemed brutal to be wading into the bill of fare with poor old Bicky headed
17 | for the breadline.
18 |
19 | {%- endmark %}
20 |
21 | When I got back old Chiswick had gone to bed, but Bicky was there, hunched up
22 | in an arm-chair, brooding pretty tensely, with a cigarette hanging out of the
23 | corner of his mouth and a more or less glassy stare in his eyes. He had the
24 | aspect of one who had been soaked with what the newspaper chappies call "some
25 | blunt instrument."
26 |
27 | "This is a bit thick, old thing—what!" I said.
28 |
29 | He picked up his glass and drained it feverishly, overlooking the fact that it
30 | hadn't anything in it.
31 |
32 | "I'm done, Bertie!" he said.
33 |
34 | He had another go at the glass. It didn't seem to do him any good.
35 |
36 | "If only this had happened a week later, Bertie! My next month's money was due
37 | to roll in on Saturday. I could have worked a wheeze I've been reading about
38 | in the magazine advertisements. It seems that you can make a dashed amount of
39 | money if you can only collect a few dollars and start a chicken-farm. Jolly
40 | sound scheme, Bertie! Say you buy a hen—call it one hen for the sake of
41 | argument. It lays an egg every day of the week. You sell the eggs seven for
42 | twenty-five cents. Keep of hen costs nothing. Profit practically twenty-five
43 | cents on every seven eggs. Or look at it another way: Suppose you have a dozen
44 | eggs. Each of the hens has a dozen chickens. The chickens grow up and have
45 | more chickens. Why, in no time you'd have the place covered knee-deep in hens,
46 | all laying eggs, at twenty-five cents for every seven. You'd make a fortune.
47 | Jolly life, too, keeping hens!" He had begun to get quite worked up at the
48 | thought of it, but he slopped back in his chair at this juncture with a good
49 | deal of gloom. "But, of course, it's no good," he said, "because I haven't the
50 | cash."
51 |
52 | "You've only to say the word, you know, Bicky, old top."
53 |
54 | "Thanks awfully, Bertie, but I'm not going to sponge on you."
55 |
56 | That's always the way in this world. The chappies you'd like to lend money to
57 | won't let you, whereas the chappies you don't want to lend it to will do
58 | everything except actually stand you on your head and lift the specie out of
59 | your pockets. As a lad who has always rolled tolerably free in the right
60 | stuff, I've had lots of experience of the second class. Many's the time, back
61 | in London, I've hurried along Piccadilly and felt the hot breath of the
62 | toucher on the back of my neck and heard his sharp, excited yapping as he
63 | closed in on me. I've simply spent my life scattering largesse to blighters I
64 | didn't care a hang for; yet here was I now, dripping doubloons and pieces of
65 | eight and longing to hand them over, and Bicky, poor fish, absolutely on his
66 | uppers, not taking any at any price.
67 |
68 | "Well, there's only one hope, then."
69 |
70 | "What's that?"
71 |
72 | "Jeeves."
73 |
74 | "Sir?"
75 |
76 | There was Jeeves, standing behind me, full of zeal. In this matter of
77 | shimmering into rooms the chappie is rummy to a degree. You're sitting in the
78 | old armchair, thinking of this and that, and then suddenly you look up, and
79 | there he is. He moves from point to point with as little uproar as a jelly
80 | fish. The thing startled poor old Bicky considerably. He rose from his seat
81 | like a rocketing pheasant. I'm used to Jeeves now, but often in the days when
82 | he first came to me I've bitten my tongue freely on finding him unexpectedly
83 | in my midst.
84 |
85 | [My Man Jeeves by PG Wodehouse][MMJ]
86 |
87 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
--------------------------------------------------------------------------------
/hyde/tests/sites/test_tagger/content/blog/another-sad-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: Another Sad Post
3 | description: >
4 | Something else sad happened.
5 | created: !!timestamp '2011-03-01 10:00:00'
6 | tags:
7 | - sad
8 | - events
9 | ---
10 |
11 | {% mark excerpt -%}
12 |
13 | I went and dressed sadly. It will show you pretty well how pipped I was when I
14 | tell you that I near as a toucher put on a white tie with a dinner-jacket. I
15 | sallied out for a bit of food more to pass the time than because I wanted it.
16 | It seemed brutal to be wading into the bill of fare with poor old Bicky headed
17 | for the breadline.
18 |
19 | {%- endmark %}
20 |
21 | When I got back old Chiswick had gone to bed, but Bicky was there, hunched up
22 | in an arm-chair, brooding pretty tensely, with a cigarette hanging out of the
23 | corner of his mouth and a more or less glassy stare in his eyes. He had the
24 | aspect of one who had been soaked with what the newspaper chappies call "some
25 | blunt instrument."
26 |
27 | "This is a bit thick, old thing—what!" I said.
28 |
29 | He picked up his glass and drained it feverishly, overlooking the fact that it
30 | hadn't anything in it.
31 |
32 | "I'm done, Bertie!" he said.
33 |
34 | He had another go at the glass. It didn't seem to do him any good.
35 |
36 | "If only this had happened a week later, Bertie! My next month's money was due
37 | to roll in on Saturday. I could have worked a wheeze I've been reading about
38 | in the magazine advertisements. It seems that you can make a dashed amount of
39 | money if you can only collect a few dollars and start a chicken-farm. Jolly
40 | sound scheme, Bertie! Say you buy a hen—call it one hen for the sake of
41 | argument. It lays an egg every day of the week. You sell the eggs seven for
42 | twenty-five cents. Keep of hen costs nothing. Profit practically twenty-five
43 | cents on every seven eggs. Or look at it another way: Suppose you have a dozen
44 | eggs. Each of the hens has a dozen chickens. The chickens grow up and have
45 | more chickens. Why, in no time you'd have the place covered knee-deep in hens,
46 | all laying eggs, at twenty-five cents for every seven. You'd make a fortune.
47 | Jolly life, too, keeping hens!" He had begun to get quite worked up at the
48 | thought of it, but he slopped back in his chair at this juncture with a good
49 | deal of gloom. "But, of course, it's no good," he said, "because I haven't the
50 | cash."
51 |
52 | "You've only to say the word, you know, Bicky, old top."
53 |
54 | "Thanks awfully, Bertie, but I'm not going to sponge on you."
55 |
56 | That's always the way in this world. The chappies you'd like to lend money to
57 | won't let you, whereas the chappies you don't want to lend it to will do
58 | everything except actually stand you on your head and lift the specie out of
59 | your pockets. As a lad who has always rolled tolerably free in the right
60 | stuff, I've had lots of experience of the second class. Many's the time, back
61 | in London, I've hurried along Piccadilly and felt the hot breath of the
62 | toucher on the back of my neck and heard his sharp, excited yapping as he
63 | closed in on me. I've simply spent my life scattering largesse to blighters I
64 | didn't care a hang for; yet here was I now, dripping doubloons and pieces of
65 | eight and longing to hand them over, and Bicky, poor fish, absolutely on his
66 | uppers, not taking any at any price.
67 |
68 | "Well, there's only one hope, then."
69 |
70 | "What's that?"
71 |
72 | "Jeeves."
73 |
74 | "Sir?"
75 |
76 | There was Jeeves, standing behind me, full of zeal. In this matter of
77 | shimmering into rooms the chappie is rummy to a degree. You're sitting in the
78 | old armchair, thinking of this and that, and then suddenly you look up, and
79 | there he is. He moves from point to point with as little uproar as a jelly
80 | fish. The thing startled poor old Bicky considerably. He rose from his seat
81 | like a rocketing pheasant. I'm used to Jeeves now, but often in the days when
82 | he first came to me I've bitten my tongue freely on finding him unexpectedly
83 | in my midst.
84 |
85 | [My Man Jeeves by PG Wodehouse][MMJ]
86 |
87 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
--------------------------------------------------------------------------------
/hyde/tests/ext/test_combine.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Use nose
4 | `$ pip install nose`
5 | `$ nosetests`
6 | """
7 | from hyde.generator import Generator
8 | from hyde.site import Site
9 |
10 | from fswrap import File, Folder
11 |
12 | COMBINE_SOURCE = File(__file__).parent.child_folder('combine')
13 | TEST_SITE = File(__file__).parent.parent.child_folder('_test')
14 |
15 | class CombineTester(object):
16 | def _test_combine(self, content):
17 | s = Site(TEST_SITE)
18 | s.config.plugins = [
19 | 'hyde.ext.plugins.meta.MetaPlugin',
20 | 'hyde.ext.plugins.structure.CombinePlugin']
21 | source = TEST_SITE.child('content/media/js/script.js')
22 | target = File(Folder(s.config.deploy_root_path).child('media/js/script.js'))
23 | File(source).write(content)
24 |
25 | gen = Generator(s)
26 | gen.generate_resource_at_path(source)
27 |
28 | assert target.exists
29 | text = target.read_all()
30 | return text, s
31 |
32 | class TestCombine(CombineTester):
33 |
34 | def setUp(self):
35 | TEST_SITE.make()
36 | TEST_SITE.parent.child_folder(
37 | 'sites/test_jinja').copy_contents_to(TEST_SITE)
38 | TEST_SITE.child_folder('content/media/js').make()
39 | COMBINE_SOURCE.copy_contents_to(TEST_SITE.child('content/media/js'))
40 |
41 | def tearDown(self):
42 | TEST_SITE.delete()
43 |
44 | def test_combine_top(self):
45 | text, _ = self._test_combine("""
46 | ---
47 | combine:
48 | files: script.*.js
49 | where: top
50 | ---
51 |
52 | Last line""")
53 | assert text == """var a = 1 + 2;
54 | var b = a + 3;
55 | var c = a + 5;
56 | Last line"""
57 | return
58 |
59 | def test_combine_bottom(self):
60 | text, _ = self._test_combine("""
61 | ---
62 | combine:
63 | files: script.*.js
64 | where: bottom
65 | ---
66 |
67 | First line
68 | """)
69 | expected = """First line
70 | var a = 1 + 2;
71 | var b = a + 3;
72 | var c = a + 5;
73 | """
74 |
75 | assert text.strip() == expected.strip()
76 | return
77 |
78 | def test_combine_bottom_unsorted(self):
79 | text, _ = self._test_combine("""
80 | ---
81 | combine:
82 | sort: false
83 | files:
84 | - script.3.js
85 | - script.1.js
86 | - script.2.js
87 | where: bottom
88 | ---
89 |
90 | First line
91 | """)
92 | expected = """First line
93 | var c = a + 5;
94 | var a = 1 + 2;
95 | var b = a + 3;
96 | """
97 |
98 | assert text.strip() == expected.strip()
99 | return
100 |
101 | def test_combine_remove(self):
102 | _, s = self._test_combine("""
103 | ---
104 | combine:
105 | files: script.*.js
106 | remove: yes
107 | ---
108 |
109 | First line""")
110 | for i in range(1,4):
111 | assert not File(Folder(s.config.deploy_root_path).
112 | child('media/js/script.%d.js' % i)).exists
113 |
114 |
115 | class TestCombinePaths(CombineTester):
116 |
117 | def setUp(self):
118 | TEST_SITE.make()
119 | TEST_SITE.parent.child_folder(
120 | 'sites/test_jinja').copy_contents_to(TEST_SITE)
121 | TEST_SITE.child_folder('content/media/js').make()
122 | JS = TEST_SITE.child_folder('content/scripts').make()
123 | S1 = JS.child_folder('s1').make()
124 | S2 = JS.child_folder('s2').make()
125 | S3 = JS.child_folder('s3').make()
126 | File(COMBINE_SOURCE.child('script.1.js')).copy_to(S1)
127 | File(COMBINE_SOURCE.child('script.2.js')).copy_to(S2)
128 | File(COMBINE_SOURCE.child('script.3.js')).copy_to(S3)
129 |
130 | def tearDown(self):
131 | TEST_SITE.delete()
132 |
133 | def test_combine_top(self):
134 | text, _ = self._test_combine("""
135 | ---
136 | combine:
137 | root: scripts
138 | recurse: true
139 | files: script.*.js
140 | where: top
141 | ---
142 |
143 | Last line""")
144 | assert text == """var a = 1 + 2;
145 | var b = a + 3;
146 | var c = a + 5;
147 | Last line"""
148 | return
149 |
--------------------------------------------------------------------------------
/hyde/tests/sites/test_sorter/content/blog/sad-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: A Sad Post
3 | description: >
4 | Something sad happened.
5 | created: !!timestamp '2010-12-01 10:00:00'
6 | ---
7 |
8 | {% mark image -%}
9 |
10 | 
11 |
12 | {%- endmark %}
13 |
14 | {% mark excerpt -%}
15 |
16 | I went and dressed sadly. It will show you pretty well how pipped I was when I
17 | tell you that I near as a toucher put on a white tie with a dinner-jacket. I
18 | sallied out for a bit of food more to pass the time than because I wanted it.
19 | It seemed brutal to be wading into the bill of fare with poor old Bicky headed
20 | for the breadline.
21 |
22 | {%- endmark %}
23 |
24 | When I got back old Chiswick had gone to bed, but Bicky was there, hunched up
25 | in an arm-chair, brooding pretty tensely, with a cigarette hanging out of the
26 | corner of his mouth and a more or less glassy stare in his eyes. He had the
27 | aspect of one who had been soaked with what the newspaper chappies call "some
28 | blunt instrument."
29 |
30 | "This is a bit thick, old thing—what!" I said.
31 |
32 | He picked up his glass and drained it feverishly, overlooking the fact that it
33 | hadn't anything in it.
34 |
35 | "I'm done, Bertie!" he said.
36 |
37 | He had another go at the glass. It didn't seem to do him any good.
38 |
39 | "If only this had happened a week later, Bertie! My next month's money was due
40 | to roll in on Saturday. I could have worked a wheeze I've been reading about
41 | in the magazine advertisements. It seems that you can make a dashed amount of
42 | money if you can only collect a few dollars and start a chicken-farm. Jolly
43 | sound scheme, Bertie! Say you buy a hen—call it one hen for the sake of
44 | argument. It lays an egg every day of the week. You sell the eggs seven for
45 | twenty-five cents. Keep of hen costs nothing. Profit practically twenty-five
46 | cents on every seven eggs. Or look at it another way: Suppose you have a dozen
47 | eggs. Each of the hens has a dozen chickens. The chickens grow up and have
48 | more chickens. Why, in no time you'd have the place covered knee-deep in hens,
49 | all laying eggs, at twenty-five cents for every seven. You'd make a fortune.
50 | Jolly life, too, keeping hens!" He had begun to get quite worked up at the
51 | thought of it, but he slopped back in his chair at this juncture with a good
52 | deal of gloom. "But, of course, it's no good," he said, "because I haven't the
53 | cash."
54 |
55 | "You've only to say the word, you know, Bicky, old top."
56 |
57 | "Thanks awfully, Bertie, but I'm not going to sponge on you."
58 |
59 | That's always the way in this world. The chappies you'd like to lend money to
60 | won't let you, whereas the chappies you don't want to lend it to will do
61 | everything except actually stand you on your head and lift the specie out of
62 | your pockets. As a lad who has always rolled tolerably free in the right
63 | stuff, I've had lots of experience of the second class. Many's the time, back
64 | in London, I've hurried along Piccadilly and felt the hot breath of the
65 | toucher on the back of my neck and heard his sharp, excited yapping as he
66 | closed in on me. I've simply spent my life scattering largesse to blighters I
67 | didn't care a hang for; yet here was I now, dripping doubloons and pieces of
68 | eight and longing to hand them over, and Bicky, poor fish, absolutely on his
69 | uppers, not taking any at any price.
70 |
71 | "Well, there's only one hope, then."
72 |
73 | "What's that?"
74 |
75 | "Jeeves."
76 |
77 | "Sir?"
78 |
79 | There was Jeeves, standing behind me, full of zeal. In this matter of
80 | shimmering into rooms the chappie is rummy to a degree. You're sitting in the
81 | old armchair, thinking of this and that, and then suddenly you look up, and
82 | there he is. He moves from point to point with as little uproar as a jelly
83 | fish. The thing startled poor old Bicky considerably. He rose from his seat
84 | like a rocketing pheasant. I'm used to Jeeves now, but often in the days when
85 | he first came to me I've bitten my tongue freely on finding him unexpectedly
86 | in my midst.
87 |
88 | [My Man Jeeves by PG Wodehouse][MMJ]
89 |
90 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
91 |
--------------------------------------------------------------------------------
/hyde/tests/test_initialize.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Use nose
4 | `$ pip install nose`
5 | `$ nosetests`
6 | """
7 |
8 |
9 | from hyde.engine import Engine
10 | from hyde.exceptions import HydeException
11 | from hyde.layout import Layout
12 |
13 | from fswrap import File, Folder
14 | from nose.tools import raises, with_setup, nottest
15 |
16 | TEST_SITE = File(__file__).parent.child_folder('_test')
17 | TEST_SITE_AT_USER = Folder('~/_test')
18 |
19 | @nottest
20 | def create_test_site():
21 | TEST_SITE.make()
22 |
23 | @nottest
24 | def delete_test_site():
25 | TEST_SITE.delete()
26 |
27 | @nottest
28 | def create_test_site_at_user():
29 | TEST_SITE_AT_USER.make()
30 |
31 | @nottest
32 | def delete_test_site_at_user():
33 | TEST_SITE_AT_USER.delete()
34 |
35 | @raises(HydeException)
36 | @with_setup(create_test_site, delete_test_site)
37 | def test_ensure_exception_when_site_yaml_exists():
38 | e = Engine(raise_exceptions=True)
39 | File(TEST_SITE.child('site.yaml')).write("Hey")
40 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))
41 |
42 | @raises(HydeException)
43 | @with_setup(create_test_site, delete_test_site)
44 | def test_ensure_exception_when_content_folder_exists():
45 | e = Engine(raise_exceptions=True)
46 | TEST_SITE.child_folder('content').make()
47 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))
48 |
49 | @raises(HydeException)
50 | @with_setup(create_test_site, delete_test_site)
51 | def test_ensure_exception_when_layout_folder_exists():
52 | e = Engine(raise_exceptions=True)
53 | TEST_SITE.child_folder('layout').make()
54 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))
55 |
56 | @with_setup(create_test_site, delete_test_site)
57 | def test_ensure_no_exception_when_empty_site_exists():
58 | e = Engine(raise_exceptions=True)
59 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))
60 | verify_site_contents(TEST_SITE, Layout.find_layout())
61 |
62 | @with_setup(create_test_site, delete_test_site)
63 | def test_ensure_no_exception_when_forced():
64 | e = Engine(raise_exceptions=True)
65 | TEST_SITE.child_folder('layout').make()
66 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
67 | verify_site_contents(TEST_SITE, Layout.find_layout())
68 | TEST_SITE.delete()
69 | TEST_SITE.child_folder('content').make()
70 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
71 | verify_site_contents(TEST_SITE, Layout.find_layout())
72 | TEST_SITE.delete()
73 | TEST_SITE.make()
74 | File(TEST_SITE.child('site.yaml')).write("Hey")
75 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
76 | verify_site_contents(TEST_SITE, Layout.find_layout())
77 |
78 | @with_setup(create_test_site, delete_test_site)
79 | def test_ensure_no_exception_when_sitepath_does_not_exist():
80 | e = Engine(raise_exceptions=True)
81 | TEST_SITE.delete()
82 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
83 | verify_site_contents(TEST_SITE, Layout.find_layout())
84 |
85 | @with_setup(create_test_site_at_user, delete_test_site_at_user)
86 | def test_ensure_can_create_site_at_user():
87 | e = Engine(raise_exceptions=True)
88 | TEST_SITE_AT_USER.delete()
89 | e.run(e.parse(['-s', unicode(TEST_SITE_AT_USER), 'create', '-f']))
90 | verify_site_contents(TEST_SITE_AT_USER, Layout.find_layout())
91 |
92 | @nottest
93 | def verify_site_contents(site, layout):
94 | assert site.exists
95 | assert site.child_folder('layout').exists
96 | assert File(site.child('info.yaml')).exists
97 |
98 | expected = map(lambda f: f.get_relative_path(layout), layout.walker.walk_all())
99 | actual = map(lambda f: f.get_relative_path(site), site.walker.walk_all())
100 | assert actual
101 | assert expected
102 |
103 | expected.sort()
104 | actual.sort()
105 | assert actual == expected
106 |
107 | @raises(HydeException)
108 | @with_setup(create_test_site, delete_test_site)
109 | def test_ensure_exception_when_layout_is_invalid():
110 | e = Engine(raise_exceptions=True)
111 | e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-l', 'junk']))
112 |
113 |
--------------------------------------------------------------------------------
/hyde/layouts/basic/content/blog/sad-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: A Sad Post
3 | description: >
4 | Something sad happened.
5 | created: !!timestamp '2010-12-01 10:00:00'
6 | tags:
7 | - sad
8 | - thoughts
9 | ---
10 |
11 | {% mark image -%}
12 |
13 | 
14 |
15 | {%- endmark %}
16 |
17 | {% mark excerpt -%}
18 |
19 | I went and dressed sadly. It will show you pretty well how pipped I was when I
20 | tell you that I near as a toucher put on a white tie with a dinner-jacket. I
21 | sallied out for a bit of food more to pass the time than because I wanted it.
22 | It seemed brutal to be wading into the bill of fare with poor old Bicky headed
23 | for the breadline.
24 |
25 | {%- endmark %}
26 |
27 | When I got back old Chiswick had gone to bed, but Bicky was there, hunched up
28 | in an arm-chair, brooding pretty tensely, with a cigarette hanging out of the
29 | corner of his mouth and a more or less glassy stare in his eyes. He had the
30 | aspect of one who had been soaked with what the newspaper chappies call "some
31 | blunt instrument."
32 |
33 | "This is a bit thick, old thing—what!" I said.
34 |
35 | He picked up his glass and drained it feverishly, overlooking the fact that it
36 | hadn't anything in it.
37 |
38 | "I'm done, Bertie!" he said.
39 |
40 | He had another go at the glass. It didn't seem to do him any good.
41 |
42 | "If only this had happened a week later, Bertie! My next month's money was due
43 | to roll in on Saturday. I could have worked a wheeze I've been reading about
44 | in the magazine advertisements. It seems that you can make a dashed amount of
45 | money if you can only collect a few dollars and start a chicken-farm. Jolly
46 | sound scheme, Bertie! Say you buy a hen—call it one hen for the sake of
47 | argument. It lays an egg every day of the week. You sell the eggs seven for
48 | twenty-five cents. Keep of hen costs nothing. Profit practically twenty-five
49 | cents on every seven eggs. Or look at it another way: Suppose you have a dozen
50 | eggs. Each of the hens has a dozen chickens. The chickens grow up and have
51 | more chickens. Why, in no time you'd have the place covered knee-deep in hens,
52 | all laying eggs, at twenty-five cents for every seven. You'd make a fortune.
53 | Jolly life, too, keeping hens!" He had begun to get quite worked up at the
54 | thought of it, but he slopped back in his chair at this juncture with a good
55 | deal of gloom. "But, of course, it's no good," he said, "because I haven't the
56 | cash."
57 |
58 | "You've only to say the word, you know, Bicky, old top."
59 |
60 | "Thanks awfully, Bertie, but I'm not going to sponge on you."
61 |
62 | That's always the way in this world. The chappies you'd like to lend money to
63 | won't let you, whereas the chappies you don't want to lend it to will do
64 | everything except actually stand you on your head and lift the specie out of
65 | your pockets. As a lad who has always rolled tolerably free in the right
66 | stuff, I've had lots of experience of the second class. Many's the time, back
67 | in London, I've hurried along Piccadilly and felt the hot breath of the
68 | toucher on the back of my neck and heard his sharp, excited yapping as he
69 | closed in on me. I've simply spent my life scattering largesse to blighters I
70 | didn't care a hang for; yet here was I now, dripping doubloons and pieces of
71 | eight and longing to hand them over, and Bicky, poor fish, absolutely on his
72 | uppers, not taking any at any price.
73 |
74 | "Well, there's only one hope, then."
75 |
76 | "What's that?"
77 |
78 | "Jeeves."
79 |
80 | "Sir?"
81 |
82 | There was Jeeves, standing behind me, full of zeal. In this matter of
83 | shimmering into rooms the chappie is rummy to a degree. You're sitting in the
84 | old armchair, thinking of this and that, and then suddenly you look up, and
85 | there he is. He moves from point to point with as little uproar as a jelly
86 | fish. The thing startled poor old Bicky considerably. He rose from his seat
87 | like a rocketing pheasant. I'm used to Jeeves now, but often in the days when
88 | he first came to me I've bitten my tongue freely on finding him unexpectedly
89 | in my midst.
90 |
91 | [My Man Jeeves by PG Wodehouse][MMJ]
92 |
93 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
--------------------------------------------------------------------------------
/hyde/tests/sites/test_paginator/content/blog/sad-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: A Sad Post
3 | description: >
4 | Something sad happened.
5 | created: !!timestamp '2010-12-01 10:00:00'
6 | tags:
7 | - sad
8 | - thoughts
9 | ---
10 |
11 | {% mark image -%}
12 |
13 | 
14 |
15 | {%- endmark %}
16 |
17 | {% mark excerpt -%}
18 |
19 | I went and dressed sadly. It will show you pretty well how pipped I was when I
20 | tell you that I near as a toucher put on a white tie with a dinner-jacket. I
21 | sallied out for a bit of food more to pass the time than because I wanted it.
22 | It seemed brutal to be wading into the bill of fare with poor old Bicky headed
23 | for the breadline.
24 |
25 | {%- endmark %}
26 |
27 | When I got back old Chiswick had gone to bed, but Bicky was there, hunched up
28 | in an arm-chair, brooding pretty tensely, with a cigarette hanging out of the
29 | corner of his mouth and a more or less glassy stare in his eyes. He had the
30 | aspect of one who had been soaked with what the newspaper chappies call "some
31 | blunt instrument."
32 |
33 | "This is a bit thick, old thing—what!" I said.
34 |
35 | He picked up his glass and drained it feverishly, overlooking the fact that it
36 | hadn't anything in it.
37 |
38 | "I'm done, Bertie!" he said.
39 |
40 | He had another go at the glass. It didn't seem to do him any good.
41 |
42 | "If only this had happened a week later, Bertie! My next month's money was due
43 | to roll in on Saturday. I could have worked a wheeze I've been reading about
44 | in the magazine advertisements. It seems that you can make a dashed amount of
45 | money if you can only collect a few dollars and start a chicken-farm. Jolly
46 | sound scheme, Bertie! Say you buy a hen—call it one hen for the sake of
47 | argument. It lays an egg every day of the week. You sell the eggs seven for
48 | twenty-five cents. Keep of hen costs nothing. Profit practically twenty-five
49 | cents on every seven eggs. Or look at it another way: Suppose you have a dozen
50 | eggs. Each of the hens has a dozen chickens. The chickens grow up and have
51 | more chickens. Why, in no time you'd have the place covered knee-deep in hens,
52 | all laying eggs, at twenty-five cents for every seven. You'd make a fortune.
53 | Jolly life, too, keeping hens!" He had begun to get quite worked up at the
54 | thought of it, but he slopped back in his chair at this juncture with a good
55 | deal of gloom. "But, of course, it's no good," he said, "because I haven't the
56 | cash."
57 |
58 | "You've only to say the word, you know, Bicky, old top."
59 |
60 | "Thanks awfully, Bertie, but I'm not going to sponge on you."
61 |
62 | That's always the way in this world. The chappies you'd like to lend money to
63 | won't let you, whereas the chappies you don't want to lend it to will do
64 | everything except actually stand you on your head and lift the specie out of
65 | your pockets. As a lad who has always rolled tolerably free in the right
66 | stuff, I've had lots of experience of the second class. Many's the time, back
67 | in London, I've hurried along Piccadilly and felt the hot breath of the
68 | toucher on the back of my neck and heard his sharp, excited yapping as he
69 | closed in on me. I've simply spent my life scattering largesse to blighters I
70 | didn't care a hang for; yet here was I now, dripping doubloons and pieces of
71 | eight and longing to hand them over, and Bicky, poor fish, absolutely on his
72 | uppers, not taking any at any price.
73 |
74 | "Well, there's only one hope, then."
75 |
76 | "What's that?"
77 |
78 | "Jeeves."
79 |
80 | "Sir?"
81 |
82 | There was Jeeves, standing behind me, full of zeal. In this matter of
83 | shimmering into rooms the chappie is rummy to a degree. You're sitting in the
84 | old armchair, thinking of this and that, and then suddenly you look up, and
85 | there he is. He moves from point to point with as little uproar as a jelly
86 | fish. The thing startled poor old Bicky considerably. He rose from his seat
87 | like a rocketing pheasant. I'm used to Jeeves now, but often in the days when
88 | he first came to me I've bitten my tongue freely on finding him unexpectedly
89 | in my midst.
90 |
91 | [My Man Jeeves by PG Wodehouse][MMJ]
92 |
93 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
--------------------------------------------------------------------------------
/hyde/tests/sites/test_tagger/content/blog/sad-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: A Sad Post
3 | description: >
4 | Something sad happened.
5 | created: !!timestamp '2010-12-01 10:00:00'
6 | tags:
7 | - sad
8 | - thoughts
9 | - transform
10 | ---
11 |
12 | {% mark image -%}
13 |
14 | 
15 |
16 | {%- endmark %}
17 |
18 | {% mark excerpt -%}
19 |
20 | I went and dressed sadly. It will show you pretty well how pipped I was when I
21 | tell you that I near as a toucher put on a white tie with a dinner-jacket. I
22 | sallied out for a bit of food more to pass the time than because I wanted it.
23 | It seemed brutal to be wading into the bill of fare with poor old Bicky headed
24 | for the breadline.
25 |
26 | {%- endmark %}
27 |
28 | When I got back old Chiswick had gone to bed, but Bicky was there, hunched up
29 | in an arm-chair, brooding pretty tensely, with a cigarette hanging out of the
30 | corner of his mouth and a more or less glassy stare in his eyes. He had the
31 | aspect of one who had been soaked with what the newspaper chappies call "some
32 | blunt instrument."
33 |
34 | "This is a bit thick, old thing—what!" I said.
35 |
36 | He picked up his glass and drained it feverishly, overlooking the fact that it
37 | hadn't anything in it.
38 |
39 | "I'm done, Bertie!" he said.
40 |
41 | He had another go at the glass. It didn't seem to do him any good.
42 |
43 | "If only this had happened a week later, Bertie! My next month's money was due
44 | to roll in on Saturday. I could have worked a wheeze I've been reading about
45 | in the magazine advertisements. It seems that you can make a dashed amount of
46 | money if you can only collect a few dollars and start a chicken-farm. Jolly
47 | sound scheme, Bertie! Say you buy a hen—call it one hen for the sake of
48 | argument. It lays an egg every day of the week. You sell the eggs seven for
49 | twenty-five cents. Keep of hen costs nothing. Profit practically twenty-five
50 | cents on every seven eggs. Or look at it another way: Suppose you have a dozen
51 | eggs. Each of the hens has a dozen chickens. The chickens grow up and have
52 | more chickens. Why, in no time you'd have the place covered knee-deep in hens,
53 | all laying eggs, at twenty-five cents for every seven. You'd make a fortune.
54 | Jolly life, too, keeping hens!" He had begun to get quite worked up at the
55 | thought of it, but he slopped back in his chair at this juncture with a good
56 | deal of gloom. "But, of course, it's no good," he said, "because I haven't the
57 | cash."
58 |
59 | "You've only to say the word, you know, Bicky, old top."
60 |
61 | "Thanks awfully, Bertie, but I'm not going to sponge on you."
62 |
63 | That's always the way in this world. The chappies you'd like to lend money to
64 | won't let you, whereas the chappies you don't want to lend it to will do
65 | everything except actually stand you on your head and lift the specie out of
66 | your pockets. As a lad who has always rolled tolerably free in the right
67 | stuff, I've had lots of experience of the second class. Many's the time, back
68 | in London, I've hurried along Piccadilly and felt the hot breath of the
69 | toucher on the back of my neck and heard his sharp, excited yapping as he
70 | closed in on me. I've simply spent my life scattering largesse to blighters I
71 | didn't care a hang for; yet here was I now, dripping doubloons and pieces of
72 | eight and longing to hand them over, and Bicky, poor fish, absolutely on his
73 | uppers, not taking any at any price.
74 |
75 | "Well, there's only one hope, then."
76 |
77 | "What's that?"
78 |
79 | "Jeeves."
80 |
81 | "Sir?"
82 |
83 | There was Jeeves, standing behind me, full of zeal. In this matter of
84 | shimmering into rooms the chappie is rummy to a degree. You're sitting in the
85 | old armchair, thinking of this and that, and then suddenly you look up, and
86 | there he is. He moves from point to point with as little uproar as a jelly
87 | fish. The thing startled poor old Bicky considerably. He rose from his seat
88 | like a rocketing pheasant. I'm used to Jeeves now, but often in the days when
89 | he first came to me I've bitten my tongue freely on finding him unexpectedly
90 | in my midst.
91 |
92 | [My Man Jeeves by PG Wodehouse][MMJ]
93 |
94 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
--------------------------------------------------------------------------------
/hyde/tests/ext/test_paginator.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Use nose
4 | `$ pip install nose`
5 | `$ nosetests`
6 | """
7 | from textwrap import dedent
8 |
9 | from hyde.generator import Generator
10 | from hyde.site import Site
11 |
12 | from fswrap import File
13 |
14 | TEST_SITE = File(__file__).parent.parent.child_folder('_test')
15 |
16 | class TestPaginator(object):
17 |
18 | def setUp(self):
19 | TEST_SITE.make()
20 | TEST_SITE.parent.child_folder(
21 | 'sites/test_paginator').copy_contents_to(TEST_SITE)
22 | self.s = Site(TEST_SITE)
23 | self.deploy = TEST_SITE.child_folder('deploy')
24 |
25 | self.gen = Generator(self.s)
26 | self.gen.load_site_if_needed()
27 | self.gen.load_template_if_needed()
28 | self.gen.generate_all()
29 |
30 |
31 | def tearDown(self):
32 | TEST_SITE.delete()
33 |
34 |
35 | def test_pages_of_one(self):
36 | pages = ['pages_of_one.txt', 'page2/pages_of_one.txt',
37 | 'page3/pages_of_one.txt', 'page4/pages_of_one.txt']
38 | files = [File(self.deploy.child(p)) for p in pages]
39 | for f in files:
40 | assert f.exists
41 |
42 | page5 = File(self.deploy.child('page5/pages_of_one.txt'))
43 | assert not page5.exists
44 |
45 |
46 | def test_pages_of_one_content(self):
47 | expected_page1_content = dedent('''\
48 | Another Sad Post
49 |
50 | /page2/pages_of_one.txt''')
51 | expected_page2_content = dedent('''\
52 | A Happy Post
53 | /pages_of_one.txt
54 | /page3/pages_of_one.txt''')
55 | expected_page3_content = dedent('''\
56 | An Angry Post
57 | /page2/pages_of_one.txt
58 | /page4/pages_of_one.txt''')
59 | expected_page4_content = dedent('''\
60 | A Sad Post
61 | /page3/pages_of_one.txt
62 | ''')
63 |
64 | page1 = self.deploy.child('pages_of_one.txt')
65 | content = File(page1).read_all()
66 | assert expected_page1_content == content
67 |
68 | page2 = self.deploy.child('page2/pages_of_one.txt')
69 | content = File(page2).read_all()
70 | assert expected_page2_content == content
71 |
72 | page3 = self.deploy.child('page3/pages_of_one.txt')
73 | content = File(page3).read_all()
74 | assert expected_page3_content == content
75 |
76 | page4 = self.deploy.child('page4/pages_of_one.txt')
77 | content = File(page4).read_all()
78 | assert expected_page4_content == content
79 |
80 |
81 | def test_pages_of_ten(self):
82 | page1 = self.deploy.child('pages_of_ten.txt')
83 | page2 = self.deploy.child('page2/pages_of_ten.txt')
84 |
85 | assert File(page1).exists
86 | assert not File(page2).exists
87 |
88 |
89 | def test_pages_of_ten_depends(self):
90 | depends = self.gen.deps['pages_of_ten.txt']
91 |
92 | assert depends
93 | assert len(depends) == 4
94 | assert 'blog/sad-post.html' in depends
95 | assert 'blog/another-sad-post.html' in depends
96 | assert 'blog/angry-post.html' in depends
97 | assert 'blog/happy-post.html' in depends
98 |
99 |
100 | def test_pages_of_ten_content(self):
101 | expected_content = dedent('''\
102 | Another Sad Post
103 | A Happy Post
104 | An Angry Post
105 | A Sad Post
106 | ''')
107 |
108 | page = self.deploy.child('pages_of_ten.txt')
109 | content = File(page).read_all()
110 | assert expected_content == content
111 |
112 |
113 | def test_pages_of_one_depends(self):
114 | depends = self.gen.deps['pages_of_one.txt']
115 |
116 | assert depends
117 | assert len(depends) == 4
118 | assert 'blog/sad-post.html' in depends
119 | assert 'blog/another-sad-post.html' in depends
120 | assert 'blog/angry-post.html' in depends
121 | assert 'blog/happy-post.html' in depends
122 |
123 |
124 | def test_custom_file_pattern(self):
125 | page1 = self.deploy.child('custom_file_pattern.txt')
126 | page2 = self.deploy.child('custom_file_pattern-2.txt')
127 |
128 | assert File(page1).exists
129 | assert File(page2).exists
130 |
--------------------------------------------------------------------------------
/hyde/layouts/basic/layout/base.j2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {% block starthead %}{% endblock starthead %}
10 |
11 |
12 |
14 |
15 |
16 |
18 |
19 |
22 |
25 |
26 | {% block title %}{{ resource.meta.title }}{% endblock %}
27 |
28 |
29 |
30 |
31 |
32 |
33 | {% block favicons %}
34 |
36 |
37 |
38 | {% endblock favicons %}
39 |
40 | {% block css %}
41 |
42 |
43 | {% endblock css %}
44 |
45 | {% block headjs %}
46 |
48 |
49 | {% endblock headjs %}
50 | {% block endhead %}{% endblock endhead %}
51 |
52 |
53 | {% block content %}
54 |
74 |
77 | {% endblock content%}
78 | {% block js %}
79 |
80 | {% block jquery %}
81 |
82 |
83 |
84 | {% endblock jquery %}
85 |
86 | {% block scripts %}
87 | {% endblock scripts %}
88 |
89 |
90 |
94 |
95 | {% block analytics %}
96 | {% include "analytics.j2" %}
97 | {% endblock analytics %}
98 |
99 | {% endblock js %}
100 |
101 |
--------------------------------------------------------------------------------
/hyde/ext/plugins/vcs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Contains classes and utilities to extract information from repositories
4 | """
5 |
6 | from hyde.plugin import Plugin
7 |
8 | from datetime import datetime
9 | from dateutil.parser import parse
10 | import os.path
11 | import subprocess
12 |
13 |
14 | class VCSDatesPlugin(Plugin):
15 | """
16 | Base class for getting resource timestamps from VCS.
17 | """
18 | def __init__(self, site, vcs_name='vcs'):
19 | super(VCSDatesPlugin, self).__init__(site)
20 | self.vcs_name = vcs_name
21 |
22 | def begin_site(self):
23 | for node in self.site.content.walk():
24 | for resource in node.resources:
25 | created = None
26 | modified = None
27 | try:
28 | created = resource.meta.created
29 | modified = resource.meta.modified
30 | except AttributeError:
31 | pass
32 |
33 | # Everything is already overrided
34 | if created != self.vcs_name and modified != self.vcs_name:
35 | continue
36 |
37 | date_created, date_modified = self.get_dates(resource)
38 |
39 | if created == "git":
40 | created = date_created or \
41 | datetime.utcfromtimestamp(
42 | os.path.getctime(resource.path))
43 | created = created.replace(tzinfo=None)
44 | resource.meta.created = created
45 |
46 | if modified == "git":
47 | modified = date_modified or resource.source.last_modified
48 | modified = modified.replace(tzinfo=None)
49 | resource.meta.modified = modified
50 |
51 |
52 | def get_dates(self):
53 | """
54 | Extract creation and last modification date from the vcs and include
55 | them in the meta data if they are set to "". Creation date
56 | is put in `created` and last modification date in `modified`.
57 | """
58 | return None, None
59 |
60 | #
61 | # Git Dates
62 | #
63 | class GitDatesPlugin(VCSDatesPlugin):
64 | def __init__(self, site):
65 | super(GitDatesPlugin, self).__init__(site, 'git')
66 |
67 | def get_dates(self, resource):
68 | """
69 | Retrieve dates from git
70 | """
71 | # Run git log --pretty=%ai
72 | try:
73 | commits = subprocess.check_output([
74 | "git",
75 | "log",
76 | "--pretty=%ai",
77 | resource.path
78 | ]).split("\n")
79 | commits = commits[:-1]
80 | except subprocess.CalledProcessError:
81 | self.logger.warning("Unable to get git history for [%s]" % resource)
82 | commits = None
83 |
84 | if commits:
85 | created = parse(commits[-1].strip())
86 | modified = parse(commits[0].strip())
87 | else:
88 | self.logger.warning("No git history for [%s]" % resource)
89 | created, modified = None, None
90 |
91 | return created, modified
92 |
93 | #
94 | # Mercurial Dates
95 | #
96 | class MercurialDatesPlugin(VCSDatesPlugin):
97 |
98 | def __init__(self, site):
99 | super(MercurialDatesPlugin, self).__init__(site, 'hg')
100 |
101 | def get_dates(self, resource):
102 | """
103 | Retrieve dates from mercurial
104 | """
105 | # Run hg log --template={date|isodatesec}
106 | try:
107 | commits = subprocess.check_output([
108 | "hg", "log", "--template={date|isodatesec}\n",
109 | resource.path]).split('\n')
110 | commits = commits[:-1]
111 | except subprocess.CalledProcessError:
112 | self.logger.warning("Unable to get mercurial history for [%s]"
113 | % resource)
114 | commits = None
115 |
116 | if not commits:
117 | self.logger.warning("No mercurial history for [%s]" % resource)
118 | return None, None
119 |
120 | created = parse(commits[-1].strip())
121 | modified = parse(commits[0].strip())
122 | return created, modified
123 |
--------------------------------------------------------------------------------
/hyde/ext/publishers/pyfs.py:
--------------------------------------------------------------------------------
1 | """
2 | Contains classes and utilities that help publishing a hyde website to
3 | a filesystem using PyFilesystem FS objects.
4 |
5 | This publisher provides an easy way to publish to FTP, SFTP, WebDAV or other
6 | servers by specifying a PyFS filesystem URL. For example, the following
7 | are valid URLs that can be used with this publisher:
8 |
9 | ftp://my.server.com/~username/my_blog/
10 | dav:https://username:password@my.server.com/path/to/my/site
11 |
12 | """
13 |
14 | import getpass
15 | import hashlib
16 |
17 |
18 | from hyde.publisher import Publisher
19 |
20 | from commando.util import getLoggerWithNullHandler
21 |
22 | logger = getLoggerWithNullHandler('hyde.ext.publishers.pyfs')
23 |
24 |
25 | try:
26 | from fs.osfs import OSFS
27 | from fs.path import pathjoin
28 | from fs.opener import fsopendir
29 | except ImportError:
30 | logger.error("The PyFS publisher requires PyFilesystem v0.4 or later.")
31 | logger.error("`pip install -U fs` to get it.")
32 | raise
33 |
34 |
35 |
36 | class PyFS(Publisher):
37 |
38 | def initialize(self, settings):
39 | self.settings = settings
40 | self.url = settings.url
41 | self.check_mtime = getattr(settings,"check_mtime",False)
42 | self.check_etag = getattr(settings,"check_etag",False)
43 | if self.check_etag and not isinstance(self.check_etag,basestring):
44 | raise ValueError("check_etag must name the etag algorithm")
45 | self.prompt_for_credentials()
46 | self.fs = fsopendir(self.url)
47 |
48 | def prompt_for_credentials(self):
49 | credentials = {}
50 | if "%(username)s" in self.url:
51 | print "Username: ",
52 | credentials["username"] = raw_input().strip()
53 | if "%(password)s" in self.url:
54 | credentials["password"] = getpass.getpass("Password: ")
55 | if credentials:
56 | self.url = self.url % credentials
57 |
58 | def publish(self):
59 | super(PyFS, self).publish()
60 | deploy_fs = OSFS(self.site.config.deploy_root_path.path)
61 | for (dirnm,local_filenms) in deploy_fs.walk():
62 | logger.info("Making directory: %s",dirnm)
63 | self.fs.makedir(dirnm,allow_recreate=True)
64 | remote_fileinfos = self.fs.listdirinfo(dirnm,files_only=True)
65 | # Process each local file, to see if it needs updating.
66 | for filenm in local_filenms:
67 | filepath = pathjoin(dirnm,filenm)
68 | # Try to find an existing remote file, to compare metadata.
69 | for (nm,info) in remote_fileinfos:
70 | if nm == filenm:
71 | break
72 | else:
73 | info = {}
74 | # Skip it if the etags match
75 | if self.check_etag and "etag" in info:
76 | with deploy_fs.open(filepath,"rb") as f:
77 | local_etag = self._calculate_etag(f)
78 | if info["etag"] == local_etag:
79 | logger.info("Skipping file [etag]: %s",filepath)
80 | continue
81 | # Skip it if the mtime is more recent remotely.
82 | if self.check_mtime and "modified_time" in info:
83 | local_mtime = deploy_fs.getinfo(filepath)["modified_time"]
84 | if info["modified_time"] > local_mtime:
85 | logger.info("Skipping file [mtime]: %s",filepath)
86 | continue
87 | # Upload it to the remote filesystem.
88 | logger.info("Uploading file: %s",filepath)
89 | with deploy_fs.open(filepath,"rb") as f:
90 | self.fs.setcontents(filepath,f)
91 | # Process each remote file, to see if it needs deleting.
92 | for (filenm,info) in remote_fileinfos:
93 | filepath = pathjoin(dirnm,filenm)
94 | if filenm not in local_filenms:
95 | logger.info("Removing file: %s",filepath)
96 | self.fs.remove(filepath)
97 |
98 | def _calculate_etag(self,f):
99 | hasher = getattr(hashlib,self.check_etag.lower())()
100 | data = f.read(1024*64)
101 | while data:
102 | hasher.update(data)
103 | data = f.read(1024*64)
104 | return hasher.hexdigest()
105 |
106 |
--------------------------------------------------------------------------------
/hyde/tests/test_simple_copy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Tests the simple copy feature.
4 |
5 | In order to mark some files to simply be copied to the
6 | destination without any processing what so ever add this
7 | to the config (site.yaml for example):
8 | simple_copy:
9 | - media/css/*.css
10 | - media/js/*.js
11 | - **/*.js
12 |
13 | Matching is done with `fnmatch` module. So any `glob` that fnmatch
14 | can process is a valid pattern.
15 |
16 | Use nose
17 | `$ pip install nose`
18 | `$ nosetests`
19 | """
20 | import yaml
21 |
22 | from hyde.model import Config
23 | from hyde.site import Site
24 | from hyde.generator import Generator
25 |
26 | from fswrap import File
27 | from nose.tools import nottest
28 |
29 |
30 | TEST_SITE_ROOT = File(__file__).parent.child_folder('sites/test_jinja')
31 |
32 | class TestSimpleCopy(object):
33 | @classmethod
34 | def setup_class(cls):
35 | cls.SITE_PATH = File(__file__).parent.child_folder('sites/test_jinja_with_config')
36 | cls.SITE_PATH.make()
37 | TEST_SITE_ROOT.copy_contents_to(cls.SITE_PATH)
38 |
39 | @classmethod
40 | def teardown_class(cls):
41 | cls.SITE_PATH.delete()
42 |
43 | @nottest
44 | def setup_config(self, passthru):
45 | self.config_file = File(self.SITE_PATH.child('site.yaml'))
46 | with open(self.config_file.path) as config:
47 | conf = yaml.load(config)
48 | conf['simple_copy'] = passthru
49 | self.config = Config(sitepath=self.SITE_PATH, config_dict=conf)
50 |
51 | def test_simple_copy_basic(self):
52 | self.setup_config([
53 | 'about.html'
54 | ])
55 | s = Site(self.SITE_PATH, config=self.config)
56 | s.load()
57 | res = s.content.resource_from_relative_path('about.html')
58 | assert res
59 | assert res.simple_copy
60 |
61 | def test_simple_copy_directory(self):
62 | self.setup_config([
63 | '**/*.html'
64 | ])
65 | s = Site(self.SITE_PATH, config=self.config)
66 | s.load()
67 | res = s.content.resource_from_relative_path('about.html')
68 | assert res
69 | assert not res.simple_copy
70 | res = s.content.resource_from_relative_path('blog/2010/december/merry-christmas.html')
71 | assert res
72 | assert res.simple_copy
73 |
74 | def test_simple_copy_multiple(self):
75 | self.setup_config([
76 | '**/*.html',
77 | 'media/css/*.css'
78 | ])
79 | s = Site(self.SITE_PATH, config=self.config)
80 | s.load()
81 | res = s.content.resource_from_relative_path('about.html')
82 | assert res
83 | assert not res.simple_copy
84 | res = s.content.resource_from_relative_path('blog/2010/december/merry-christmas.html')
85 | assert res
86 | assert res.simple_copy
87 | res = s.content.resource_from_relative_path('media/css/site.css')
88 | assert res
89 | assert res.simple_copy
90 |
91 | def test_generator(self):
92 | self.setup_config([
93 | '**/*.html',
94 | 'media/css/*.css'
95 | ])
96 | s = Site(self.SITE_PATH, self.config)
97 | g = Generator(s)
98 | g.generate_all()
99 | source = s.content.resource_from_relative_path('blog/2010/december/merry-christmas.html')
100 | target = File(s.config.deploy_root_path.child(source.relative_deploy_path))
101 | left = source.source_file.read_all()
102 | right = target.read_all()
103 | assert left == right
104 |
105 | def test_plugins(self):
106 |
107 | text = """
108 | ---
109 | title: Hey
110 | author: Me
111 | twitter: @me
112 | ---
113 | {%% extends "base.html" %%}
114 |
115 | {%% block main %%}
116 | Hi!
117 |
118 | I am a test template to make sure jinja2 generation works well with hyde.
119 | {{resource.meta.title}}
120 | {{resource.meta.author}}
121 | {{resource.meta.twitter}}
122 | {%% endblock %%}
123 | """
124 | index = File(self.SITE_PATH.child('content/blog/index.html'))
125 | index.write(text)
126 | self.setup_config([
127 | '**/*.html',
128 | 'media/css/*.css'
129 | ])
130 | conf = {'plugins': ['hyde.ext.plugins.meta.MetaPlugin']}
131 | conf.update(self.config.to_dict())
132 | s = Site(self.SITE_PATH, Config(sitepath=self.SITE_PATH, config_dict=conf))
133 | g = Generator(s)
134 | g.generate_all()
135 | source = s.content.resource_from_relative_path('blog/index.html')
136 | target = File(s.config.deploy_root_path.child(source.relative_deploy_path))
137 | left = source.source_file.read_all()
138 | right = target.read_all()
139 | assert left == right
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Version 0.8.8
2 |
3 | A brand new **hyde**
4 | ====================
5 |
6 | This is the new version of `hyde`_ under active development.
7 | `Hyde documentation`_ is a work in progress.
8 |
9 | `Hyde starter kit`_ by `merlinrebrovic`_ is a really nice way to get started
10 | with hyde.
11 |
12 | `Hyde layout for bootstrap`_ by `auzigog`_ is also a good alternative if you
13 | like Twitter's `bootstrap framework`_.
14 |
15 | You can also take a look at `Hyde Powered Websites`_ for inspiration and reference.
16 |
17 | Installation
18 | ------------
19 |
20 | To get the latest released version:
21 |
22 | ::
23 |
24 | pip install hyde
25 |
26 | For the current trunk:
27 |
28 | ::
29 |
30 | pip install -e git://github.com/hyde/hyde.git#egg=hyde
31 |
32 | Creating a new hyde site
33 | ------------------------
34 |
35 | The following command:
36 |
37 | ::
38 |
39 | hyde -s ~/test_site create
40 |
41 | will create a new hyde site using the test layout.
42 |
43 | Generating the hyde site
44 | ------------------------
45 |
46 | ::
47 |
48 | cd ~/test_site
49 | hyde gen
50 |
51 | Serving the website
52 | -------------------
53 |
54 | ::
55 |
56 | cd ~/test_site
57 | hyde serve
58 | open http://localhost:8080
59 |
60 | Publishing the website
61 | ----------------------
62 |
63 | ::
64 |
65 | cd ~/test_site
66 | hyde publish -p github
67 |
68 |
69 | Hyde supports extensible publishers.
70 |
71 | Github
72 | ~~~~~~~
73 |
74 | The hyde documentation is published to github pages using this command with
75 | the following configuration:
76 |
77 | ::
78 |
79 | publisher:
80 | github:
81 | type: hyde.ext.publishers.dvcs.Git
82 | path: ../hyde.github.com
83 | url: git@github.com:hyde/hyde.github.com.git
84 |
85 | .. Note:: Currently, the initial path must have clone of the repository
86 | already in place for this command to work.
87 |
88 | PyFS
89 | ~~~~~~~
90 |
91 | Hyde also has a publisher that acts as a frontend to the awesome
92 | `PyFS library`_ (thanks to `rfk`_). Here are a few configuration
93 | options for some PyFS backends:
94 |
95 | ::
96 |
97 | publisher:
98 | zip:
99 | type: hyde.ext.publishers.pyfs.PyFS
100 | url: zip://~/deploy/hyde/docs.zip
101 | s3:
102 | type: hyde.ext.publishers.pyfs.PyFS
103 | url: s3://hyde/docs
104 | sftp:
105 | type: hyde.ext.publishers.pyfs.PyFS
106 | url: sftp:hydeuser:hydepassword@hydedocs.org
107 |
108 | .. Note:: PyFS is not installed with hyde. In order to use the
109 | PyFS publisher, you need to install pyfs separately.
110 |
111 | Any PyFS dependencies (Example: `boto` for S3 publishing)
112 | need to be installed separately as well.
113 |
114 | ::
115 |
116 | pip install fs
117 | pip install boto
118 |
119 | To get additional help on PyFS backends, you can run the following
120 | command once PyFS is installed:
121 |
122 | ::
123 |
124 | fsls --listopeners
125 |
126 | Examples
127 | --------
128 |
129 | 1. `Hyde Documentation Source`_
130 | 2. `Cloudpanic`_
131 | 3. `Ringce`_
132 |
133 | A brief list of features
134 | --------------------------
135 |
136 | 1. Evented Plugins: The Plugin hooks allow plugins to listen to events
137 | that occur during different times in the lifecycle and respond
138 | accordingly.
139 | 2. Metadata: Hyde now supports hierarchical metadata. You can specify
140 | and override variables at the site, node or the page level and access
141 | them in the templates.
142 | 3. Organization: The sorter, grouper and tagger plugins provide rich
143 | meta-data driven organizational capabilities to hyde sites.
144 | 4. Publishing: Hyde sites can be published to variety of targets including
145 | github pages, Amazon S3 & SFTP.
146 |
147 | Links
148 | -----
149 |
150 | 1. `Changelog`_
151 | 2. `Authors`_
152 |
153 |
154 | .. _hyde: https://github.com/lakshmivyas/hyde
155 | .. _Hyde documentation: http://hyde.github.com
156 | .. _Hyde Documentation Source: https://github.com/hyde/docs
157 | .. _Cloudpanic: https://github.com/tipiirai/cloudpanic
158 | .. _Ringce: https://github.com/lakshmivyas/ringce/tree/v3.0
159 | .. _Authors: https://github.com/hyde/hyde/blob/master/AUTHORS.rst
160 | .. _Changelog: https://github.com/hyde/hyde/blob/master/CHANGELOG.rst
161 | .. _Hyde starter kit: http://merlin.rebrovic.net/hyde-starter-kit/about.html
162 | .. _merlinrebrovic: https://github.com/merlinrebrovic
163 | .. _rfk: https://github.com/rfk
164 | .. _PyFS library: http://packages.python.org/fs/
165 | .. _Hyde layout for bootstrap: https://github.com/auzigog/hyde-bootstrap
166 | .. _auzigog: https://github.com/auzigog
167 | .. _bootstrap framework: http://twitter.github.com/bootstrap/
168 | .. _Hyde Powered Websites: https://github.com/hyde/hyde/wiki/Hyde-Powered
169 |
--------------------------------------------------------------------------------
/hyde/template.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # pylint: disable-msg=W0104,E0602,W0613,R0201
3 | """
4 | Abstract classes and utilities for template engines
5 | """
6 | from hyde.exceptions import HydeException
7 |
8 | import abc
9 |
10 | from commando.util import getLoggerWithNullHandler
11 |
12 |
13 | class HtmlWrap(object):
14 | """
15 | A wrapper class for raw html.
16 |
17 | Provides pyquery interface if available.
18 | Otherwise raw html access.
19 | """
20 |
21 | def __init__(self, html):
22 | super(HtmlWrap, self).__init__()
23 | self.raw = html
24 | try:
25 | from pyquery import PyQuery
26 | except:
27 | PyQuery = False
28 | if PyQuery:
29 | self.q = PyQuery(html)
30 |
31 | def __unicode__(self):
32 | return self.raw
33 |
34 | def __call__(self, selector=None):
35 | if not self.q:
36 | return self.raw
37 | return self.q(selector).html()
38 |
39 | class Template(object):
40 | """
41 | Interface for hyde template engines. To use a different template engine,
42 | the following interface must be implemented.
43 | """
44 |
45 | __metaclass__ = abc.ABCMeta
46 |
47 | def __init__(self, sitepath):
48 | self.sitepath = sitepath
49 | self.logger = getLoggerWithNullHandler(self.__class__.__name__)
50 |
51 | @abc.abstractmethod
52 | def configure(self, site, engine):
53 |
54 | """
55 | The site object should contain a config attribute. The config object
56 | is a simple YAML object with required settings. The template
57 | implementations are responsible for transforming this object to match
58 | the `settings` required for the template engines.
59 |
60 | The engine is an informal protocol to provide access to some
61 | hyde internals.
62 |
63 | The preprocessor attribute must contain the function that trigger the
64 | hyde plugins to preprocess the template after load.
65 |
66 | A context_for_path attribute must contain the function that returns
67 | the context object that is populated with the appropriate variables
68 | for the given path.
69 | """
70 | return
71 |
72 | def clear_caches(self):
73 | """
74 | Clear all caches to prepare for regeneration
75 | """
76 | return
77 |
78 | def get_dependencies(self, text):
79 | """
80 | Finds the dependencies based on the included
81 | files.
82 | """
83 | return None
84 |
85 | @abc.abstractmethod
86 | def render_resource(self, resource, context):
87 | """
88 | This function must load the file represented by the resource
89 | object and return the rendered text.
90 | """
91 | return ''
92 |
93 | @abc.abstractmethod
94 | def render(self, text, context):
95 | """
96 | Given the text, and the context, this function must return the
97 | rendered string.
98 | """
99 |
100 | return ''
101 |
102 | @abc.abstractproperty
103 | def exception_class(self):
104 | return HydeException
105 |
106 | @abc.abstractproperty
107 | def patterns(self):
108 | """
109 | Patterns for matching selected template statements.
110 | """
111 | return {}
112 |
113 | @abc.abstractmethod
114 | def get_include_statement(self, path_to_include):
115 | """
116 | Returns an include statement for the current template,
117 | given the path to include.
118 | """
119 | return '{%% include \'%s\' %%}' % path_to_include
120 |
121 | @abc.abstractmethod
122 | def get_extends_statement(self, path_to_extend):
123 | """
124 | Returns an extends statement for the current template,
125 | given the path to extend.
126 | """
127 | return '{%% extends \'%s\' %%}' % path_to_extend
128 |
129 | @abc.abstractmethod
130 | def get_open_tag(self, tag, params):
131 | """
132 | Returns an open tag statement.
133 | """
134 | return '{%% %s %s %%}' % (tag, params)
135 |
136 | @abc.abstractmethod
137 | def get_close_tag(self, tag, params):
138 | """
139 | Returns an open tag statement.
140 | """
141 | return '{%% end%s %%}' % tag
142 |
143 | @abc.abstractmethod
144 | def get_content_url_statement(self, url):
145 | """
146 | Returns the content url statement.
147 | """
148 | return '/' + url
149 |
150 | @abc.abstractmethod
151 | def get_media_url_statement(self, url):
152 | """
153 | Returns the media url statement.
154 | """
155 | return '/media/' + url
156 |
157 | @staticmethod
158 | def find_template(site):
159 | """
160 | Reads the configuration to find the appropriate template.
161 | """
162 | # TODO: Find the appropriate template environment
163 | from hyde.ext.templates.jinja import Jinja2Template
164 | template = Jinja2Template(site.sitepath)
165 | return template
166 |
--------------------------------------------------------------------------------
/hyde/tests/sites/test_sorter/content/blog/angry-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: An Angry Post
3 | description: >
4 | Temper. Temper. Temper.
5 | created: !!timestamp '2011-01-01 10:00:00'
6 | index: 1
7 | ---
8 |
9 | {% mark excerpt -%}
10 |
11 | To complete the character-study of Mr. Worple, he was a man of extremely
12 | uncertain temper, and his general tendency was to think that Corky was a poor
13 | chump and that whatever step he took in any direction on his own account, was
14 | just another proof of his innate idiocy. I should imagine Jeeves feels very
15 | much the same about me.
16 |
17 | {%- endmark %}
18 |
19 | So when Corky trickled into my apartment one afternoon, shooing a girl in
20 | front of him, and said, "Bertie, I want you to meet my fiancée, Miss Singer,"
21 | the aspect of the matter which hit me first was precisely the one which he had
22 | come to consult me about. The very first words I spoke were, "Corky, how about
23 | your uncle?"
24 |
25 | The poor chap gave one of those mirthless laughs. He was looking anxious and
26 | worried, like a man who has done the murder all right but can't think what the
27 | deuce to do with the body.
28 |
29 | "We're so scared, Mr. Wooster," said the girl. "We were hoping that you might
30 | suggest a way of breaking it to him."
31 |
32 | Muriel Singer was one of those very quiet, appealing girls who have a way of
33 | looking at you with their big eyes as if they thought you were the greatest
34 | thing on earth and wondered that you hadn't got on to it yet yourself. She sat
35 | there in a sort of shrinking way, looking at me as if she were saying to
36 | herself, "Oh, I do hope this great strong man isn't going to hurt me." She
37 | gave a fellow a protective kind of feeling, made him want to stroke her hand
38 | and say, "There, there, little one!" or words to that effect. She made me feel
39 | that there was nothing I wouldn't do for her. She was rather like one of those
40 | innocent-tasting American drinks which creep imperceptibly into your system so
41 | that, before you know what you're doing, you're starting out to reform the
42 | world by force if necessary and pausing on your way to tell the large man in
43 | the corner that, if he looks at you like that, you will knock his head off.
44 | What I mean is, she made me feel alert and dashing, like a jolly old
45 | knight-errant or something of that kind. I felt that I was with her in this
46 | thing to the limit.
47 |
48 | "I don't see why your uncle shouldn't be most awfully bucked," I said to
49 | Corky. "He will think Miss Singer the ideal wife for you."
50 |
51 | Corky declined to cheer up.
52 |
53 | "You don't know him. Even if he did like Muriel he wouldn't admit it. That's
54 | the sort of pig-headed guy he is. It would be a matter of principle with him
55 | to kick. All he would consider would be that I had gone and taken an important
56 | step without asking his advice, and he would raise Cain automatically. He's
57 | always done it."
58 |
59 | I strained the old bean to meet this emergency.
60 |
61 | "You want to work it so that he makes Miss Singer's acquaintance without
62 | knowing that you know her. Then you come along"
63 |
64 | "But how can I work it that way?"
65 |
66 | I saw his point. That was the catch.
67 |
68 | "There's only one thing to do," I said.
69 |
70 | "What's that?"
71 |
72 | "Leave it to Jeeves."
73 |
74 | And I rang the bell.
75 |
76 | "Sir?" said Jeeves, kind of manifesting himself. One of the rummy things about
77 | Jeeves is that, unless you watch like a hawk, you very seldom see him come
78 | into a room. He's like one of those weird chappies in India who dissolve
79 | themselves into thin air and nip through space in a sort of disembodied way
80 | and assemble the parts again just where they want them. I've got a cousin
81 | who's what they call a Theosophist, and he says he's often nearly worked the
82 | thing himself, but couldn't quite bring it off, probably owing to having fed
83 | in his boyhood on the flesh of animals slain in anger and pie.
84 |
85 | The moment I saw the man standing there, registering respectful attention, a
86 | weight seemed to roll off my mind. I felt like a lost child who spots his
87 | father in the offing. There was something about him that gave me confidence.
88 |
89 | Jeeves is a tallish man, with one of those dark, shrewd faces. His eye gleams
90 | with the light of pure intelligence.
91 |
92 | "Jeeves, we want your advice."
93 |
94 | "Very good, sir."
95 |
96 | I boiled down Corky's painful case into a few well-chosen words.
97 |
98 | "So you see what it amount to, Jeeves. We want you to suggest some way by
99 | which Mr. Worple can make Miss Singer's acquaintance without getting on to the
100 | fact that Mr. Corcoran already knows her. Understand?"
101 |
102 | "Perfectly, sir."
103 |
104 | "Well, try to think of something."
105 |
106 | "I have thought of something already, sir."
107 |
108 | "You have!"
109 |
110 | "The scheme I would suggest cannot fail of success, but it has what may seem
111 | to you a drawback, sir, in that it requires a certain financial outlay."
112 |
113 | "He means," I translated to Corky, "that he has got a pippin of an idea, but
114 | it's going to cost a bit."
115 |
116 | Naturally the poor chap's face dropped, for this seemed to dish the whole
117 | thing. But I was still under the influence of the girl's melting gaze, and I
118 | saw that this was where I started in as a knight-errant.
119 |
120 | "You can count on me for all that sort of thing, Corky," I said. "Only too
121 | glad. Carry on, Jeeves."
122 |
123 | "I would suggest, sir, that Mr. Corcoran take advantage of Mr. Worple's
124 | attachment to ornithology."
125 |
126 | "How on earth did you know that he was fond of birds?"
127 |
128 |
129 | [My Man Jeeves by PG Wodehouse][MMJ]
130 |
131 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
132 |
--------------------------------------------------------------------------------
/hyde/tests/sites/test_paginator/content/blog/angry-post.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: An Angry Post
3 | description: >
4 | Temper. Temper. Temper.
5 | created: !!timestamp '2011-01-01 10:00:00'
6 | tags:
7 | - angry
8 | - thoughts
9 | ---
10 |
11 | {% mark excerpt -%}
12 |
13 | To complete the character-study of Mr. Worple, he was a man of extremely
14 | uncertain temper, and his general tendency was to think that Corky was a poor
15 | chump and that whatever step he took in any direction on his own account, was
16 | just another proof of his innate idiocy. I should imagine Jeeves feels very
17 | much the same about me.
18 |
19 | {%- endmark %}
20 |
21 | So when Corky trickled into my apartment one afternoon, shooing a girl in
22 | front of him, and said, "Bertie, I want you to meet my fiancée, Miss Singer,"
23 | the aspect of the matter which hit me first was precisely the one which he had
24 | come to consult me about. The very first words I spoke were, "Corky, how about
25 | your uncle?"
26 |
27 | The poor chap gave one of those mirthless laughs. He was looking anxious and
28 | worried, like a man who has done the murder all right but can't think what the
29 | deuce to do with the body.
30 |
31 | "We're so scared, Mr. Wooster," said the girl. "We were hoping that you might
32 | suggest a way of breaking it to him."
33 |
34 | Muriel Singer was one of those very quiet, appealing girls who have a way of
35 | looking at you with their big eyes as if they thought you were the greatest
36 | thing on earth and wondered that you hadn't got on to it yet yourself. She sat
37 | there in a sort of shrinking way, looking at me as if she were saying to
38 | herself, "Oh, I do hope this great strong man isn't going to hurt me." She
39 | gave a fellow a protective kind of feeling, made him want to stroke her hand
40 | and say, "There, there, little one!" or words to that effect. She made me feel
41 | that there was nothing I wouldn't do for her. She was rather like one of those
42 | innocent-tasting American drinks which creep imperceptibly into your system so
43 | that, before you know what you're doing, you're starting out to reform the
44 | world by force if necessary and pausing on your way to tell the large man in
45 | the corner that, if he looks at you like that, you will knock his head off.
46 | What I mean is, she made me feel alert and dashing, like a jolly old
47 | knight-errant or something of that kind. I felt that I was with her in this
48 | thing to the limit.
49 |
50 | "I don't see why your uncle shouldn't be most awfully bucked," I said to
51 | Corky. "He will think Miss Singer the ideal wife for you."
52 |
53 | Corky declined to cheer up.
54 |
55 | "You don't know him. Even if he did like Muriel he wouldn't admit it. That's
56 | the sort of pig-headed guy he is. It would be a matter of principle with him
57 | to kick. All he would consider would be that I had gone and taken an important
58 | step without asking his advice, and he would raise Cain automatically. He's
59 | always done it."
60 |
61 | I strained the old bean to meet this emergency.
62 |
63 | "You want to work it so that he makes Miss Singer's acquaintance without
64 | knowing that you know her. Then you come along"
65 |
66 | "But how can I work it that way?"
67 |
68 | I saw his point. That was the catch.
69 |
70 | "There's only one thing to do," I said.
71 |
72 | "What's that?"
73 |
74 | "Leave it to Jeeves."
75 |
76 | And I rang the bell.
77 |
78 | "Sir?" said Jeeves, kind of manifesting himself. One of the rummy things about
79 | Jeeves is that, unless you watch like a hawk, you very seldom see him come
80 | into a room. He's like one of those weird chappies in India who dissolve
81 | themselves into thin air and nip through space in a sort of disembodied way
82 | and assemble the parts again just where they want them. I've got a cousin
83 | who's what they call a Theosophist, and he says he's often nearly worked the
84 | thing himself, but couldn't quite bring it off, probably owing to having fed
85 | in his boyhood on the flesh of animals slain in anger and pie.
86 |
87 | The moment I saw the man standing there, registering respectful attention, a
88 | weight seemed to roll off my mind. I felt like a lost child who spots his
89 | father in the offing. There was something about him that gave me confidence.
90 |
91 | Jeeves is a tallish man, with one of those dark, shrewd faces. His eye gleams
92 | with the light of pure intelligence.
93 |
94 | "Jeeves, we want your advice."
95 |
96 | "Very good, sir."
97 |
98 | I boiled down Corky's painful case into a few well-chosen words.
99 |
100 | "So you see what it amount to, Jeeves. We want you to suggest some way by
101 | which Mr. Worple can make Miss Singer's acquaintance without getting on to the
102 | fact that Mr. Corcoran already knows her. Understand?"
103 |
104 | "Perfectly, sir."
105 |
106 | "Well, try to think of something."
107 |
108 | "I have thought of something already, sir."
109 |
110 | "You have!"
111 |
112 | "The scheme I would suggest cannot fail of success, but it has what may seem
113 | to you a drawback, sir, in that it requires a certain financial outlay."
114 |
115 | "He means," I translated to Corky, "that he has got a pippin of an idea, but
116 | it's going to cost a bit."
117 |
118 | Naturally the poor chap's face dropped, for this seemed to dish the whole
119 | thing. But I was still under the influence of the girl's melting gaze, and I
120 | saw that this was where I started in as a knight-errant.
121 |
122 | "You can count on me for all that sort of thing, Corky," I said. "Only too
123 | glad. Carry on, Jeeves."
124 |
125 | "I would suggest, sir, that Mr. Corcoran take advantage of Mr. Worple's
126 | attachment to ornithology."
127 |
128 | "How on earth did you know that he was fond of birds?"
129 |
130 |
131 | [My Man Jeeves by PG Wodehouse][MMJ]
132 |
133 | [MMJ]: http://www.gutenberg.org/cache/epub/8164/pg8164.html
--------------------------------------------------------------------------------