Documented virtual paths
This commit is contained in:
parent
06ac62c715
commit
b3ff365a8a
|
@ -29,22 +29,35 @@ Most plugins will not have source objects that actually originate on the
|
||||||
file system. This means that their "source" is entirely virtual. Because
|
file system. This means that their "source" is entirely virtual. Because
|
||||||
this is a very common situation there is a base class, the
|
this is a very common situation there is a base class, the
|
||||||
`VirtualSourceObject` which plugins can subclass. The constructor takes one
|
`VirtualSourceObject` which plugins can subclass. The constructor takes one
|
||||||
argument which is the parent source object the virtual source lives below.
|
argument which is the source record the virtual source lives below. Virtual
|
||||||
|
sources are separated from the records they belong to with an at sign (`@`)
|
||||||
|
in the path. The part after the at sign is called the “virtual path”.
|
||||||
|
|
||||||
|
For instance in the below example the canonical path for the object would be
|
||||||
|
the record's path + `@source`. So if the record was `/hello` then the
|
||||||
|
path would be `/hello@source`. The true base record it belongs to can be
|
||||||
|
referenced from the [record :ref](record/) property.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from lektor.sourceobj import VirtualSourceObject
|
from lektor.sourceobj import VirtualSourceObject
|
||||||
|
from lektor.utils import build_url
|
||||||
|
|
||||||
class Source(VirtualSourceObject):
|
class Source(VirtualSourceObject):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self.record.path + '@source'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_content(self):
|
def source_content(self):
|
||||||
with open(self.parent.source_filename) as f:
|
with open(self.record.source_filename) as f:
|
||||||
return f.read().decode('utf-8')
|
return f.read().decode('utf-8')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url_path(self):
|
def url_path(self):
|
||||||
return self.parent.url_path + 'source.txt'
|
return build_url([self.record.url_path, 'source.txt'])
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information see [add-build-program
|
For more information see [add-build-program
|
||||||
:ref](../../environment/add-build-program/).
|
:ref](../../environment/add-build-program/) as well as
|
||||||
|
[virtualpathresolver :ref](../../environment/virtualpathresolver/).
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
title: record
|
||||||
|
---
|
||||||
|
summary: A reference to the associated base record.
|
||||||
|
---
|
||||||
|
type: property
|
||||||
|
---
|
||||||
|
version_added: 2.0
|
||||||
|
---
|
||||||
|
body:
|
||||||
|
|
||||||
|
Most records (other than the root) have a [parent :ref](../parent/). This as
|
||||||
|
a concept generally makes in most situations. However when virtual sources
|
||||||
|
come into play the parent often just refers to another virtual source but not
|
||||||
|
the root record they all belong to.
|
||||||
|
|
||||||
|
This property for the most part refers back to the object itself. However
|
||||||
|
for virtual sources this refers back to the record that was associated with
|
||||||
|
the virtual source. Most of the time this matches `parent` but for nested
|
||||||
|
source objects it's useful to be able to refer back to the base in all cases
|
||||||
|
without having to walk up the parents.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```html+jinja
|
||||||
|
{% if this != this.record %}
|
||||||
|
<a href="{{ this.record|url }}">go back to overview</a>
|
||||||
|
{% endif %}
|
||||||
|
```
|
|
@ -17,7 +17,8 @@ The path needs to be absolute with folder separated by slashes.
|
||||||
|
|
||||||
The default behavior is to load the unpaginated version of a record. If you
|
The default behavior is to load the unpaginated version of a record. If you
|
||||||
want to select a specific page for pagination, then you need to pass
|
want to select a specific page for pagination, then you need to pass
|
||||||
`page_num` with a valid page number.
|
`page_num` with a valid page number or you use the virtual path (`@1` for the
|
||||||
|
first page for instance).
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -34,3 +35,15 @@ Here another example that loads the current page but in another language:
|
||||||
{% set other_lang = site.get(this._path, alt='ru') %}
|
{% set other_lang = site.get(this._path, alt='ru') %}
|
||||||
<p>This page in Russian: {{ other_lang.title }}
|
<p>This page in Russian: {{ other_lang.title }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Virtual Paths
|
||||||
|
|
||||||
|
This method can also be used to look up [virtual paths
|
||||||
|
:ref](../../../../content/paths/). For instance to fetch a specific version of a
|
||||||
|
pagination you can use a virtual path instead of using the `page_num`
|
||||||
|
parameter:
|
||||||
|
|
||||||
|
```pycon
|
||||||
|
>>> pad.get('/blog@3')
|
||||||
|
<Page model=u'blog' path='/blog' page_num=3>
|
||||||
|
```
|
||||||
|
|
|
@ -29,7 +29,8 @@ The following attributes exist on the pagination object:
|
||||||
| `for_page()` | Returns the record for a different page.
|
| `for_page()` | Returns the record for a different page.
|
||||||
|
|
||||||
The `for_page()` function accepts a page number and returns the record for
|
The `for_page()` function accepts a page number and returns the record for
|
||||||
the other page.
|
the other page. For more information also see the virtual path example
|
||||||
|
below.
|
||||||
|
|
||||||
!! *Changed in Lektor 2.0:* The `for_page()` method was added.
|
!! *Changed in Lektor 2.0:* The `for_page()` method was added.
|
||||||
|
|
||||||
|
@ -66,3 +67,23 @@ next page link as well as the number of the current page:
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Virtual Paths
|
||||||
|
|
||||||
|
!! *New in Lektor 2.0:* virtual paths did not exist in earlier Lektor versions.
|
||||||
|
|
||||||
|
The pagination is implemented in a way where each page in the pagination is
|
||||||
|
a virtual path below the record itself. The value of the path is just the
|
||||||
|
number of the page. So for instance to link to the second page you can just
|
||||||
|
do this:
|
||||||
|
|
||||||
|
```html+jinja
|
||||||
|
<a href="{{ '@2'|url }}">Go to Page 2</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively you can also use the `for_path()` function which returns the
|
||||||
|
entire pagination for a page:
|
||||||
|
|
||||||
|
```html+jinja
|
||||||
|
<a href="{{ this.pagination.for_page(2)|url }}">Go to Page 2</a>
|
||||||
|
```
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
title: parent
|
|
||||||
---
|
|
||||||
summary: Returns the parent record for a record.
|
|
||||||
---
|
|
||||||
type: property
|
|
||||||
---
|
|
||||||
body:
|
|
||||||
|
|
||||||
Because Lektor's database is a tree all records with the exception of the
|
|
||||||
root record have a parent. This can be accessed with this property.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```html+jinja
|
|
||||||
<a href="{{ this.parent|url }}">Up to {{ this.parent.title }}</a>
|
|
||||||
```
|
|
|
@ -19,17 +19,22 @@ artifacts out of source objects.
|
||||||
```python
|
```python
|
||||||
from lektor.sourceobj import VirtualSourceObject
|
from lektor.sourceobj import VirtualSourceObject
|
||||||
from lektor.build_programs import BuildProgram
|
from lektor.build_programs import BuildProgram
|
||||||
|
from lektor.utils import build_url
|
||||||
|
|
||||||
class Source(VirtualSourceObject):
|
class Source(VirtualSourceObject):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self.record.path + '@source'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_content(self):
|
def source_content(self):
|
||||||
with open(self.parent.source_filename) as f:
|
with open(self.record.source_filename) as f:
|
||||||
return f.read().decode('utf-8')
|
return f.read().decode('utf-8')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url_path(self):
|
def url_path(self):
|
||||||
return self.parent.url_path + 'source.txt'
|
return build_url([self.record.url_path, 'source.txt'])
|
||||||
|
|
||||||
class SourceBuildProgram(BuildProgram):
|
class SourceBuildProgram(BuildProgram):
|
||||||
|
|
||||||
|
@ -43,6 +48,11 @@ class SourceBuildProgram(BuildProgram):
|
||||||
this=self.source)
|
this=self.source)
|
||||||
|
|
||||||
env.add_build_program(Source, SourceBuildProgram)
|
env.add_build_program(Source, SourceBuildProgram)
|
||||||
|
|
||||||
|
@env.virtualpathresolver('source')
|
||||||
|
def resolve_virtual_path(record, pieces):
|
||||||
|
if not pieces:
|
||||||
|
return Source(record)
|
||||||
```
|
```
|
||||||
|
|
||||||
And here the example `view_source.html` template:
|
And here the example `view_source.html` template:
|
||||||
|
|
|
@ -20,12 +20,17 @@ exists for a page.
|
||||||
```python
|
```python
|
||||||
from lektor.sourceobj import VirtualSourceObject
|
from lektor.sourceobj import VirtualSourceObject
|
||||||
from lektor.db import Record
|
from lektor.db import Record
|
||||||
|
from lektor.utils import build_url
|
||||||
|
|
||||||
class Source(VirtualSourceObject):
|
class Source(VirtualSourceObject):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self.record.path + '@source'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url_path(self):
|
def url_path(self):
|
||||||
return self.parent.url_path + 'source.txt'
|
return build_url([self.record.url_path, 'source.txt'])
|
||||||
|
|
||||||
@env.generator
|
@env.generator
|
||||||
def generate_source_file(node):
|
def generate_source_file(node):
|
||||||
|
|
|
@ -19,12 +19,17 @@ URL path segments.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from lektor.sourceobj import VirtualSourceObject
|
from lektor.sourceobj import VirtualSourceObject
|
||||||
|
from lektor.utils import build_url
|
||||||
|
|
||||||
class Source(VirtualSourceObject):
|
class Source(VirtualSourceObject):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self.record.path + '@source'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url_path(self):
|
def url_path(self):
|
||||||
return self.parent.url_path + 'source.txt'
|
return build_url([self.record.url_path, 'source.txt'])
|
||||||
|
|
||||||
@env.urlresolver
|
@env.urlresolver
|
||||||
def match_source_file(node, url_path):
|
def match_source_file(node, url_path):
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
title: virtualpathresolver
|
||||||
|
---
|
||||||
|
signature: prefix
|
||||||
|
---
|
||||||
|
summary: Registers a virtual path resolver with the environment.
|
||||||
|
---
|
||||||
|
type: method
|
||||||
|
---
|
||||||
|
version_added: 2.0
|
||||||
|
---
|
||||||
|
body:
|
||||||
|
|
||||||
|
When implementing [Virtual Source Objects :ref](../../db/obj/) it's important
|
||||||
|
that you can locate them. While virtual sources do not appear as children of
|
||||||
|
pages they can be navigated to through the database pad. This is achieved
|
||||||
|
through custom virtual path resolvers.
|
||||||
|
|
||||||
|
Each source object needs to be a unique prefix which identifies all instances
|
||||||
|
of it. For instance if you want to implement a special page (like a feed)
|
||||||
|
that would exist for specific pages, then you can register the virtual path
|
||||||
|
prefix `feed` with your plugin. Though you should use more descriptive names
|
||||||
|
for your plugins as these paths are shared.
|
||||||
|
|
||||||
|
If a user then resolves the page `/my-page@feed` it would invoke your URL
|
||||||
|
resolver with the record `my-page` and an empty list (rest of the path
|
||||||
|
segments). If they would request `/my-page@feed/recent` then it would
|
||||||
|
pass `['recent']` as path segments.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Here an example that would implement a virtual source for feeds and an
|
||||||
|
associated virtual path resolver:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lektor.sourceobj import VirtualSourceObject
|
||||||
|
from lektor.utils import build_url
|
||||||
|
|
||||||
|
class Feed(VirtualSourceObject):
|
||||||
|
|
||||||
|
def __init__(self, record, version='default'):
|
||||||
|
VirtualSourceObject.__init__(self, record)
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return '%s@feed%s' % (
|
||||||
|
self.record.path,
|
||||||
|
'/' + self.version if self.version != 'default' else '',
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_path(self):
|
||||||
|
return build_url([self.record.url_path, 'feed.xml'])
|
||||||
|
|
||||||
|
|
||||||
|
def on_setup_env(self, **extra):
|
||||||
|
@self.env.virtualpathresolver('feed')
|
||||||
|
def resolve_virtual_path(record, pieces):
|
||||||
|
if not pieces:
|
||||||
|
return Feed(record)
|
||||||
|
elif pieces == ['recent']:
|
||||||
|
return Feed(record, version='recent')
|
||||||
|
```
|
|
@ -0,0 +1,32 @@
|
||||||
|
title: build_url
|
||||||
|
---
|
||||||
|
module: lektor.utils
|
||||||
|
---
|
||||||
|
signature: pieces, trailing_slash=None
|
||||||
|
---
|
||||||
|
summary: Helper function that assists in building URL paths.
|
||||||
|
---
|
||||||
|
type: function
|
||||||
|
---
|
||||||
|
version_added: 2.0
|
||||||
|
---
|
||||||
|
body:
|
||||||
|
|
||||||
|
This function assists in creating URL paths from individual segments. This
|
||||||
|
is particularly useful for building virtual source objects. It takes a bunch
|
||||||
|
of path segments and returns an absolute path. The default behavior is to
|
||||||
|
guess the trailing slash based on the presence of a dot in the last path
|
||||||
|
segment. If you want to override the detection you can explicitly pass
|
||||||
|
`True` to enforce a trailing slash or `False` to avoid it.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```pycon
|
||||||
|
>>> from lektor.utils import build_url
|
||||||
|
>>> build_url(['foo', 42])
|
||||||
|
u'/foo/42/'
|
||||||
|
>>> build_url(['foo', None, 23])
|
||||||
|
u'/foo/23/'
|
||||||
|
>>> build_url(['foo', 'hello.html'])
|
||||||
|
u'/foo/hello.html'
|
||||||
|
```
|
|
@ -0,0 +1,45 @@
|
||||||
|
title: join_path
|
||||||
|
---
|
||||||
|
module: lektor.utils
|
||||||
|
---
|
||||||
|
signature: a, b
|
||||||
|
---
|
||||||
|
summary: Joins two Lektor paths together correctly.
|
||||||
|
---
|
||||||
|
template_var:
|
||||||
|
---
|
||||||
|
type: function
|
||||||
|
---
|
||||||
|
version_added: 2.0
|
||||||
|
---
|
||||||
|
body:
|
||||||
|
|
||||||
|
Given two Lektor paths this joins them together with the rules that Lektor
|
||||||
|
set up for this. In particular this is important in the presence of
|
||||||
|
virtual paths which have their own join semantics.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```pycon
|
||||||
|
>>> from lektor.utils import join_path
|
||||||
|
>>> join_path('/blog', 'hello-world')
|
||||||
|
'/blog/hello-world'
|
||||||
|
>>> join_path('/blog', '@archive')
|
||||||
|
'/blog@archive'
|
||||||
|
>>> join_path('/blog@archive', '2015')
|
||||||
|
'/blog@archive/2015'
|
||||||
|
>>> join_path('/blog@archive/2015', '..')
|
||||||
|
'/blog@archive'
|
||||||
|
>>> join_path('/blog@archive', '..')
|
||||||
|
'/blog'
|
||||||
|
```
|
||||||
|
|
||||||
|
Special note should be taken for the numeric virtual path of paginations. It
|
||||||
|
is considered to be on the same level as the actual record:
|
||||||
|
|
||||||
|
```pycon
|
||||||
|
>>> join_path('/blog@2', '..')
|
||||||
|
'/'
|
||||||
|
>>> join_path('/blog@2', '.')
|
||||||
|
'/blog@2'
|
||||||
|
```
|
|
@ -0,0 +1,25 @@
|
||||||
|
title: parse_path
|
||||||
|
---
|
||||||
|
module: lektor.utils
|
||||||
|
---
|
||||||
|
summary: Parses a path into components.
|
||||||
|
---
|
||||||
|
type: function
|
||||||
|
---
|
||||||
|
version_added: 2.0
|
||||||
|
---
|
||||||
|
body:
|
||||||
|
|
||||||
|
This function parses a path into the individual components it's made from.
|
||||||
|
The path is always assumed to be absolute and made absolute if it's not yet
|
||||||
|
so. The root path is the empty list.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```pycon
|
||||||
|
>>> from lektor.utils import parse_path
|
||||||
|
>>> parse_path('/foo/bar')
|
||||||
|
['foo', 'bar']
|
||||||
|
>>> parse_path('/')
|
||||||
|
[]
|
||||||
|
```
|
|
@ -72,3 +72,11 @@ targeted, different files will be used. This table visualizes this:
|
||||||
| | ✓ | | fr | contents+fr.lr
|
| | ✓ | | fr | contents+fr.lr
|
||||||
| | | ✓ | en | contents+en.lr
|
| | | ✓ | en | contents+en.lr
|
||||||
| | | ✓ | fr | *missing*
|
| | | ✓ | fr | *missing*
|
||||||
|
|
||||||
|
## Alternatives and Paths
|
||||||
|
|
||||||
|
Alternatives have a special behavior with regards to paths. They alternative
|
||||||
|
code does not exist in the path! This can be confusing at first, but has the
|
||||||
|
advantage that they automatically work in most places as the paths are the
|
||||||
|
same for differnent alternatives. For more information see
|
||||||
|
[Alternatives and Paths :ref](../paths/#alternatives-and-paths).
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
title: Paths
|
||||||
|
---
|
||||||
|
summary: An explanation about how paths in Lektor work.
|
||||||
|
---
|
||||||
|
body:
|
||||||
|
|
||||||
|
!! This refers mostly to Lektor 2.0. If you are using an older version of
|
||||||
|
Lektor the virtual path feature is not implemented yet and a lot of this just
|
||||||
|
does not apply.
|
||||||
|
|
||||||
|
Lektor attempts to map the paths from the content folder as closely as possible
|
||||||
|
to the final URLs (at least in the default configuration). There are various
|
||||||
|
ways in which this can be customized (see [URLs and Slugs :ref](../urls/)) and
|
||||||
|
there are situations in which Lektor needs to render out content that does not
|
||||||
|
actually directly correspond to a path on the file system.
|
||||||
|
|
||||||
|
Here we try to explain how the path system in Lektor works and what it means.
|
||||||
|
|
||||||
|
## What is a Path
|
||||||
|
|
||||||
|
A path in Lektor is a string that uniquely identifies a [Source Object
|
||||||
|
:ref](../../api/db/obj/). For the most part these directly point to
|
||||||
|
`content.lr` files or attachments therein. These paths always use forward
|
||||||
|
slashes, no matter on which platform Lektor runs. So for instance if you
|
||||||
|
have a file named `projects/my-project/contents.lr` then the path for this
|
||||||
|
record will be `/projects/my-project`. Thereby it does not matter if the
|
||||||
|
slug or final URL was changed, the path is always the same.
|
||||||
|
|
||||||
|
Lektor uses paths to refer to records by default in all cases. This means that
|
||||||
|
if you use the [url filter :ref](../../api/templates/filters/url/) for instance
|
||||||
|
it will operate on paths and not URLs!
|
||||||
|
|
||||||
|
But what about sources that do not directly correspond to something on the
|
||||||
|
file system? This is where virtual paths come in. Virtual paths are
|
||||||
|
separated from physical paths with an at (`@`) sign. Virtual paths are always
|
||||||
|
attached to a record and point to things that are not actually coming from the
|
||||||
|
content folder but something else.
|
||||||
|
|
||||||
|
Virtual paths are for instance used to refer to paginated pages or to some
|
||||||
|
resources that plugins add.
|
||||||
|
|
||||||
|
## Virtual Paths
|
||||||
|
|
||||||
|
Virtual paths are added behind physical paths and are separated by an at
|
||||||
|
sign (`@`). The only virtual path that is supported by Lektor out of the box
|
||||||
|
is the special numeric virtual path which can be used to refer to specific
|
||||||
|
pages for a pagination. For instance `/blog@1` refers to the first page of
|
||||||
|
the `blog` page, `/blog@2` to the second etc. There are however plugins that
|
||||||
|
add virtual paths to refer to their own resources. For instance a plugin can
|
||||||
|
register `/blog@feed` to refer to a RSS/Atom feed for the blog.
|
||||||
|
|
||||||
|
## Relative Paths
|
||||||
|
|
||||||
|
Now that we talked a bit about paths, we should probably cover how relative
|
||||||
|
paths work. Relative paths work similar to how you expect them to work on
|
||||||
|
most operating systems but they can operate on the virtual as well as the
|
||||||
|
physical path. There is also some special behavior with regards to the numeric
|
||||||
|
virtual path for pagination.
|
||||||
|
|
||||||
|
For the most part `.` refers to the same page and `..` refers to the page one
|
||||||
|
level up. If you use a path that just contains of a virtual path, then it's
|
||||||
|
attached to the current page to replace the active virtual path.
|
||||||
|
|
||||||
|
| Current | Relative | Result
|
||||||
|
| --------------- | ------------ | ----------------
|
||||||
|
| `/blog` | `..` | `/`
|
||||||
|
| `/blog` | `.` | `/blog`
|
||||||
|
| `/blog/post` | `..` | `/blog`
|
||||||
|
| `/blog` | `@2` | `/blog@2`
|
||||||
|
| `/blog@2` | `@1` | `/blog@1`
|
||||||
|
| `/blog@feed` | `recent` | `/blog@feed/recent`
|
||||||
|
| `/blog@feed` | `..` | `/blog@feed`
|
||||||
|
|
||||||
|
However! The numeric path is special with regards because it's considered to
|
||||||
|
belong to the current page:
|
||||||
|
|
||||||
|
| Current | Relative | Result
|
||||||
|
| --------------- | ------------ | ----------------
|
||||||
|
| `/blog@2` | `.` | `/blog@2`
|
||||||
|
| `/blog@2` | `..` | `/`
|
||||||
|
|
||||||
|
## Alternatives and Paths
|
||||||
|
|
||||||
|
If you have used Lektor a bit you might be wondering how [Alternatives
|
||||||
|
:ref](../alts/) work with paths. The answer might be surprising but it's
|
||||||
|
basically that they don't really work with paths. Alternatives are
|
||||||
|
implemented on a level higher than paths. If you have a page that exists
|
||||||
|
in both German and English alternative then they will have the same path.
|
||||||
|
The alternative code is supplied separately.
|
||||||
|
|
||||||
|
This is done so that you can later start introducing alternatives to sites
|
||||||
|
that were never aware of them without having to go everywhere and start
|
||||||
|
passing alternative information around. While there are indeed some places
|
||||||
|
where you might have to perform some changes (especially if you perform
|
||||||
|
manual queries in the templates) for the most part adding alternatives to an
|
||||||
|
existing site later is a trivial matter.
|
|
@ -1,72 +1,45 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import posixpath
|
import posixpath
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from werkzeug.utils import cached_property
|
from werkzeug.utils import cached_property
|
||||||
|
|
||||||
from lektor.pluginsystem import Plugin
|
from lektor.pluginsystem import Plugin
|
||||||
from lektor.sourceobj import VirtualSourceObject
|
from lektor.sourceobj import VirtualSourceObject
|
||||||
from lektor.build_programs import BuildProgram
|
from lektor.build_programs import BuildProgram
|
||||||
from lektor.context import get_ctx
|
from lektor.utils import build_url, parse_path
|
||||||
|
|
||||||
|
|
||||||
def get_path_segments(str):
|
|
||||||
pieces = str.split('/')
|
|
||||||
if pieces == ['']:
|
|
||||||
return []
|
|
||||||
return pieces
|
|
||||||
|
|
||||||
|
|
||||||
def push_path(pieces, item):
|
|
||||||
if item:
|
|
||||||
pieces.append(unicode(item))
|
|
||||||
|
|
||||||
|
|
||||||
class BlogArchive(VirtualSourceObject):
|
class BlogArchive(VirtualSourceObject):
|
||||||
|
|
||||||
def __init__(self, parent, plugin, items=None, year=None, month=None):
|
def __init__(self, record, plugin, items=None):
|
||||||
VirtualSourceObject.__init__(self, parent)
|
VirtualSourceObject.__init__(self, record)
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
self._items = items
|
self._items = items
|
||||||
self.year = year
|
|
||||||
self.month = month
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date(self):
|
def path(self):
|
||||||
if self.year is None:
|
return self.record.path + '@blog-archive'
|
||||||
raise AttributeError()
|
|
||||||
return date(self.year, self.month or 1, 1)
|
template_name = 'blog-archive/index.html'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def year_archive(self):
|
def parent(self):
|
||||||
if self.year is None:
|
return self.record
|
||||||
raise AttributeError()
|
|
||||||
if self.month is None:
|
|
||||||
return self
|
|
||||||
return BlogArchive(self.parent, self.plugin, year=self.year)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def archive_index(self):
|
|
||||||
if self.year is None:
|
|
||||||
return self
|
|
||||||
return BlogArchive(self.parent, self.plugin)
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def year_archives(self):
|
def year_archives(self):
|
||||||
if self.year is not None:
|
|
||||||
return []
|
|
||||||
years = set()
|
years = set()
|
||||||
for item in self.parent.children:
|
for item in self.record.children:
|
||||||
pub_date = self.plugin.get_pub_date(item)
|
pub_date = self.plugin.get_pub_date(item)
|
||||||
if pub_date:
|
if pub_date:
|
||||||
years.add(pub_date.year)
|
years.add(pub_date.year)
|
||||||
return [BlogArchive(self.parent, self.plugin,
|
return [BlogYearArchive(self.record, self.plugin,
|
||||||
year=year) for year in sorted(years)]
|
year=year) for year in sorted(years)]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def items(self):
|
def items(self):
|
||||||
if self.year is None:
|
|
||||||
return []
|
|
||||||
if self._items is not None:
|
if self._items is not None:
|
||||||
return self._items
|
return self._items
|
||||||
rv = list(self._iter_items())
|
rv = list(self._iter_items())
|
||||||
|
@ -74,13 +47,7 @@ class BlogArchive(VirtualSourceObject):
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def _iter_items(self):
|
def _iter_items(self):
|
||||||
for item in self.parent.children:
|
return iter(())
|
||||||
pub_date = self.plugin.get_pub_date(item)
|
|
||||||
if pub_date is None:
|
|
||||||
continue
|
|
||||||
if pub_date.year == self.year and \
|
|
||||||
(self.month is None or pub_date.month == self.month):
|
|
||||||
yield item
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_any_items(self):
|
def has_any_items(self):
|
||||||
|
@ -97,34 +64,87 @@ class BlogArchive(VirtualSourceObject):
|
||||||
pub_date = self.plugin.get_pub_date(item)
|
pub_date = self.plugin.get_pub_date(item)
|
||||||
months.setdefault(date(pub_date.year, pub_date.month, 1),
|
months.setdefault(date(pub_date.year, pub_date.month, 1),
|
||||||
[]).append(item)
|
[]).append(item)
|
||||||
return [(BlogArchive(self.parent, self.plugin,
|
return [(BlogMonthArchive(self.record, self.plugin,
|
||||||
year=d.year, month=d.month), i)
|
year=d.year, month=d.month), i)
|
||||||
for d, i in sorted(months.items())]
|
for d, i in sorted(months.items())]
|
||||||
|
|
||||||
@property
|
def get_archive_url_path(self):
|
||||||
def url_path(self):
|
return self.plugin.get_url_path('archive_path')
|
||||||
prefix = self.parent.url_path.strip('/')
|
|
||||||
pieces = []
|
|
||||||
if prefix:
|
|
||||||
pieces.append(prefix)
|
|
||||||
if self.year is None:
|
|
||||||
push_path(pieces, self.plugin.get_archive_index_path())
|
|
||||||
elif self.month is None:
|
|
||||||
push_path(pieces, self.plugin.get_month_archive_prefix())
|
|
||||||
push_path(pieces, self.year)
|
|
||||||
else:
|
|
||||||
push_path(pieces, self.plugin.get_year_archive_prefix())
|
|
||||||
push_path(pieces, self.year)
|
|
||||||
push_path(pieces, self.month)
|
|
||||||
return '/%s/' % '/'.join(pieces)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def template_name(self):
|
def url_path(self):
|
||||||
if self.year is None:
|
return build_url(chain([self.record.url_path.strip('/')],
|
||||||
return 'blog-archive/index.html'
|
self.get_archive_url_path() or ()))
|
||||||
if self.month is None:
|
|
||||||
return 'blog-archive/year.html'
|
|
||||||
return 'blog-archive/month.html'
|
class BlogYearArchive(BlogArchive):
|
||||||
|
template_name = 'blog-archive/year.html'
|
||||||
|
|
||||||
|
def __init__(self, record, plugin, items=None, year=None):
|
||||||
|
BlogArchive.__init__(self, record, plugin, items)
|
||||||
|
self.year = year
|
||||||
|
|
||||||
|
def _iter_items(self):
|
||||||
|
for item in self.record.children:
|
||||||
|
pub_date = self.plugin.get_pub_date(item)
|
||||||
|
if pub_date is not None and \
|
||||||
|
pub_date.year == self.year:
|
||||||
|
yield item
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return '%s@blog-archive/%s' % (
|
||||||
|
self.record.path,
|
||||||
|
self.year,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return BlogArchive(self.record, self.plugin)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
return date(self.year, 1, 1)
|
||||||
|
|
||||||
|
def get_archive_url_path(self):
|
||||||
|
return self.plugin.get_url_path('year_archive_prefix') + [self.year]
|
||||||
|
|
||||||
|
|
||||||
|
class BlogMonthArchive(BlogArchive):
|
||||||
|
template_name = 'blog-archive/year.html'
|
||||||
|
|
||||||
|
def __init__(self, record, plugin, items=None, year=None, month=None):
|
||||||
|
BlogArchive.__init__(self, record, plugin, items)
|
||||||
|
self.year = year
|
||||||
|
self.month = month
|
||||||
|
|
||||||
|
def _iter_items(self):
|
||||||
|
for item in self.record.children:
|
||||||
|
pub_date = self.plugin.get_pub_date(item)
|
||||||
|
if pub_date is not None and \
|
||||||
|
pub_date.year == self.year and \
|
||||||
|
pub_date.month == self.month:
|
||||||
|
yield item
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return '%s@blog-archive/%s/%s' % (
|
||||||
|
self.record.path,
|
||||||
|
self.year,
|
||||||
|
self.month
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return BlogYearArchive(self.record, self.plugin, year=self.year)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
return date(self.year, self.month, 1)
|
||||||
|
|
||||||
|
def get_archive_url_path(self):
|
||||||
|
return self.plugin.get_url_path('month_archive_prefix') + [
|
||||||
|
self.year, self.month]
|
||||||
|
|
||||||
|
|
||||||
class BlogArchiveBuildProgram(BuildProgram):
|
class BlogArchiveBuildProgram(BuildProgram):
|
||||||
|
@ -150,77 +170,70 @@ class BlogArchivePlugin(Plugin):
|
||||||
def get_blog_path(self):
|
def get_blog_path(self):
|
||||||
return self.get_config().get('blog_path', '/blog')
|
return self.get_config().get('blog_path', '/blog')
|
||||||
|
|
||||||
def get_archive_index_path(self):
|
def get_url_path(self, name, default='archive'):
|
||||||
return self.get_config().get('archive_path', 'archive').strip('/')
|
return parse_path(self.get_config().get(name, default))
|
||||||
|
|
||||||
def get_year_archive_prefix(self):
|
|
||||||
return self.get_config().get('year_archive_prefix', 'archive').strip('/')
|
|
||||||
|
|
||||||
def get_month_archive_prefix(self):
|
|
||||||
return self.get_config().get('month_archive_prefix', 'archive').strip('/')
|
|
||||||
|
|
||||||
def on_setup_env(self, **extra):
|
def on_setup_env(self, **extra):
|
||||||
blog_path = self.get_blog_path()
|
|
||||||
self.env.add_build_program(BlogArchive, BlogArchiveBuildProgram)
|
self.env.add_build_program(BlogArchive, BlogArchiveBuildProgram)
|
||||||
|
|
||||||
def get_blog_archive():
|
@self.env.virtualpathresolver('blog-archive')
|
||||||
pad = get_ctx().pad
|
def blog_archive_resolver(node, pieces):
|
||||||
blog = pad.get(blog_path)
|
if node.path == self.get_blog_path():
|
||||||
if blog is not None:
|
if not pieces:
|
||||||
return BlogArchive(blog, self)
|
return BlogArchive(node, self)
|
||||||
self.env.jinja_env.globals['get_blog_archive'] = get_blog_archive
|
elif len(pieces) == 1 and pieces[0].isdigit():
|
||||||
|
return BlogYearArchive(node, self, year=int(pieces[0]))
|
||||||
|
elif len(pieces) == 2 and pieces[0].isdigit() \
|
||||||
|
and pieces[1].isdigit():
|
||||||
|
return BlogMonthArchive(node, self, year=int(pieces[0]),
|
||||||
|
month=int(pieces[1]))
|
||||||
|
|
||||||
@self.env.urlresolver
|
@self.env.urlresolver
|
||||||
def archive_resolver(node, url_path):
|
def archive_urlresolver(node, url_path):
|
||||||
if node.path != blog_path:
|
if node.path != self.get_blog_path():
|
||||||
return
|
return
|
||||||
|
|
||||||
archive_index = get_path_segments(self.get_archive_index_path())
|
archive_index = self.get_url_path('archive_path')
|
||||||
if url_path == archive_index:
|
if url_path == archive_index:
|
||||||
return BlogArchive(node, self)
|
return BlogArchive(node, self)
|
||||||
|
|
||||||
year_prefix = get_path_segments(self.get_year_archive_prefix())
|
year_prefix = self.get_url_path('year_archive_prefix')
|
||||||
month_prefix = get_path_segments(self.get_month_archive_prefix())
|
|
||||||
|
|
||||||
year = None
|
|
||||||
month = None
|
|
||||||
|
|
||||||
if url_path[:len(year_prefix)] == year_prefix and \
|
if url_path[:len(year_prefix)] == year_prefix and \
|
||||||
url_path[len(year_prefix)].isdigit() and \
|
url_path[len(year_prefix)].isdigit() and \
|
||||||
len(url_path) == len(year_prefix) + 1:
|
len(url_path) == len(year_prefix) + 1:
|
||||||
year = int(url_path[len(year_prefix)])
|
year = int(url_path[len(year_prefix)])
|
||||||
elif (url_path[:len(month_prefix)] == month_prefix and
|
rv = BlogYearArchive(node, self, year=year)
|
||||||
len(url_path) == len(month_prefix) + 2 and
|
if rv.has_any_items:
|
||||||
url_path[len(month_prefix)].isdigit() and
|
return rv
|
||||||
url_path[len(month_prefix) + 1].isdigit()):
|
|
||||||
|
month_prefix = self.get_url_path('month_archive_prefix')
|
||||||
|
if url_path[:len(month_prefix)] == month_prefix and \
|
||||||
|
len(url_path) == len(month_prefix) + 2 and \
|
||||||
|
url_path[len(month_prefix)].isdigit() and \
|
||||||
|
url_path[len(month_prefix) + 1].isdigit():
|
||||||
year = int(url_path[len(month_prefix)])
|
year = int(url_path[len(month_prefix)])
|
||||||
month = int(url_path[len(month_prefix) + 1])
|
month = int(url_path[len(month_prefix) + 1])
|
||||||
else:
|
rv = BlogMonthArchive(node, self, year=year, month=month)
|
||||||
return None
|
|
||||||
|
|
||||||
rv = BlogArchive(node, self, year=year, month=month)
|
|
||||||
if rv.has_any_items:
|
if rv.has_any_items:
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
@self.env.generator
|
@self.env.generator
|
||||||
def genererate_blog_archive_pages(source):
|
def genererate_blog_archive_pages(source):
|
||||||
if source.path != blog_path:
|
if source.path != self.get_blog_path():
|
||||||
return
|
return
|
||||||
|
|
||||||
blog = source
|
|
||||||
|
|
||||||
years = {}
|
years = {}
|
||||||
months = {}
|
months = {}
|
||||||
for post in blog.children:
|
for post in source.children:
|
||||||
pub_date = self.get_pub_date(post)
|
pub_date = self.get_pub_date(post)
|
||||||
if pub_date:
|
if pub_date:
|
||||||
years.setdefault(pub_date.year, []).append(post)
|
years.setdefault(pub_date.year, []).append(post)
|
||||||
months.setdefault((pub_date.year,
|
months.setdefault((pub_date.year,
|
||||||
pub_date.month), []).append(post)
|
pub_date.month), []).append(post)
|
||||||
|
|
||||||
yield BlogArchive(blog, self)
|
yield BlogArchive(source, self)
|
||||||
for year, items in sorted(years.items()):
|
for year, items in sorted(years.items()):
|
||||||
yield BlogArchive(blog, self, year=year, items=items)
|
yield BlogYearArchive(source, self, year=year, items=items)
|
||||||
for (year, month), items in sorted(months.items()):
|
for (year, month), items in sorted(months.items()):
|
||||||
yield BlogArchive(blog, self, year=year, month=month,
|
yield BlogMonthArchive(source, self, year=year, month=month,
|
||||||
items=items)
|
items=items)
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block blog_body %}
|
{% block blog_body %}
|
||||||
<h1>The Transcript, {{ this.date|dateformat('MMMM yyyy') }}</h1>
|
<h1>The Transcript, {{ this.date|dateformat('MMMM yyyy') }}</h1>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ this.year_archive|url }}">« Back to {{ this.year }}</a>
|
<a href="{{ '..'|url }}">« Back to {{ this.year }}</a>
|
||||||
<ul>
|
<ul>
|
||||||
{% for post in this.items %}
|
{% for post in this.items %}
|
||||||
<li><a href="{{ post|url }}">{{ post.title }}</a> by {{ post.author }} on {{ post.pub_date|dateformat }}
|
<li><a href="{{ post|url }}">{{ post.title }}</a> by {{ post.author }} on {{ post.pub_date|dateformat }}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block blog_body %}
|
{% block blog_body %}
|
||||||
<h1>The Transcript in {{ this.year }}</h1>
|
<h1>The Transcript in {{ this.year }}</h1>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ this.archive_index|url }}">« Back to the archive</a>
|
<a href="{{ '..'|url }}">« Back to the archive</a>
|
||||||
<ul>
|
<ul>
|
||||||
{% for archive, items in this.items_by_months %}
|
{% for archive, items in this.items_by_months %}
|
||||||
<li><a href="{{ archive|url }}">{{ archive.date|dateformat('MMMM') }}</a>:
|
<li><a href="{{ archive|url }}">{{ archive.date|dateformat('MMMM') }}</a>:
|
||||||
|
|
|
@ -33,8 +33,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
<h3>Missed a Post?</h3>
|
<h3>Missed a Post?</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% set archive = get_blog_archive() %}
|
<li><a href="{{ '$blog-archive'|url }}">Blog Archives</a>
|
||||||
<li><a href="{{ archive|url }}">Blog Archives</a>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue