@@ -81,10 +85,9 @@ page instead.
As a common practice, we recommend you to separate your contents and assets
(like images, downloads, etc.). We even deny access to your `content` directory
by default. If you want to use some assets (e.g. a image) in one of your content
-files, you should create an `assets` folder in Pico's root directory and upload
-your assets there. You can then access them in your Markdown using
-%base_url%/assets/ for example:
-!\[Image Title\](%base_url%/assets/image.png)
+files, use Pico's `assets` folder. You can then access them in your Markdown
+using the %assets_url% placeholder, for example:
+!\[Image Title\](%assets_url%/image.png)
### Text File Markup
@@ -117,7 +120,7 @@ classes to your theme. For example, you might want to add some CSS classes to
your theme to rule how much of the available space a image should use (e.g.
`img.small { width: 80%; }`). You can then use these CSS classes in your
Markdown files, for example:
-!\[Image Title\](%base_url%/assets/image.png) {.small}
+!\[Image Title\](%assets_url%/image.png) {.small}
There are also certain variables that you can use in your text files:
@@ -125,9 +128,15 @@ There are also certain variables that you can use in your text files:
* %base_url% - The URL to your Pico site; internal links
can be specified using %base_url%?sub/page
* %theme_url% - The URL to the currently used theme
+* %assets_url% - The URL to Pico's `assets` directory
+* %themes_url% - The URL to Pico's `themes` directory;
+ don't confuse this with %theme_url%
+* %plugins_url% - The URL to Pico's `plugins` directory
* %version% - Pico's current version string (e.g. `2.0.0`)
* %meta.*% - Access any meta variable of the current
page, e.g. %meta.author% is replaced with `Joe Bloggs`
+* %config.*% - Access any scalar config variable,
+ e.g. %config.theme% is replaced with `default`
### Blogging
@@ -150,14 +159,12 @@ something like the following:
`index.twig`), it will create a list of all your blog articles. Add the
following Twig snippet to `blog-index.twig` near `{{ content }}`:
```
- {% for page in pages|sort_by("time")|reverse %}
- {% if page.id starts with "blog/" and not page.hidden %}
-
{% endfor %}
```
@@ -175,75 +182,59 @@ details.
### Themes
-You can create themes for your Pico installation in the `themes` folder. Check
-out the default theme for an example. Pico uses [Twig][] for template
-rendering. You can select your theme by setting the `theme` option in
-`config/config.yml` to the name of your theme folder.
+You can create themes for your Pico installation in the `themes` folder. Pico
+uses [Twig][] for template rendering. You can select your theme by setting the
+`theme` option in `config/config.yml` to the name of your theme folder.
+
+[Pico's default theme][PicoTheme] isn't really intended to be used for a
+productive website, it's rather a starting point for creating your own theme.
+If the default theme isn't sufficient for you, and you don't want to create
+your own theme, you can use one of the great themes third-party developers and
+designers created in the past. As with plugins, you can find themes in
+[our Wiki][WikiThemes] and on [our website][OfficialThemes].
All themes must include an `index.twig` file to define the HTML structure of
-the theme. Below are the Twig variables that are available to use in your
-theme. Please note that paths (e.g. `{{ base_dir }}`) and URLs
-(e.g. `{{ base_url }}`) don't have a trailing slash.
+the theme, and a `pico-theme.yml` to set the necessary config parameters. Just
+refer to Pico's default theme as an example. You can use different templates
+for different content files by specifying the `Template` meta header. Simply
+add e.g. `Template: blog` to the YAML header of a content file and Pico will
+use the `blog.twig` template in your theme folder to display the page.
+
+Below are the Twig variables that are available to use in themes. Please note
+that URLs (e.g. `{{ base_url }}`) never include a trailing slash.
* `{{ site_title }}` - Shortcut to the site title (see `config/config.yml`)
* `{{ config }}` - Contains the values you set in `config/config.yml`
(e.g. `{{ config.theme }}` becomes `default`)
-* `{{ base_dir }}` - The path to your Pico root directory
* `{{ base_url }}` - The URL to your Pico site; use Twig's `link` filter to
specify internal links (e.g. `{{ "sub/page"|link }}`),
this guarantees that your link works whether URL rewriting
is enabled or not
-* `{{ theme_dir }}` - The path to the currently active theme
* `{{ theme_url }}` - The URL to the currently active theme
-* `{{ version }}` - Pico's current version string (e.g. `2.0.0`)
+* `{{ assets_url }}` - The URL to Pico's `assets` directory
+* `{{ themes_url }}` - The URL to Pico's `themes` directory; don't confuse this
+ with `{{ theme_url }}`
+* `{{ plugins_url }}` - The URL to Pico's `plugins` directory
+* `{{ version }}` - Pico's current version string (e.g. `%version%`)
* `{{ meta }}` - Contains the meta values of the current page
- * `{{ meta.title }}`
- * `{{ meta.description }}`
- * `{{ meta.author }}`
- * `{{ meta.date }}`
- * `{{ meta.date_formatted }}`
- * `{{ meta.time }}`
- * `{{ meta.robots }}`
+ * `{{ meta.title }}` - The `Title` YAML header
+ * `{{ meta.description }}` - The `Description` YAML header
+ * `{{ meta.author }}` - The `Author` YAML header
+ * `{{ meta.date }}` - The `Date` YAML header
+ * `{{ meta.date_formatted }}` - The formatted date of the page as specified
+ by the `date_format` parameter in your
+ `config/config.yml`
+ * `{{ meta.time }}` - The [Unix timestamp][UnixTimestamp] derived from the
+ `Date` YAML header
+ * `{{ meta.robots }}` - The `Robots` YAML header
* ...
* `{{ content }}` - The content of the current page after it has been processed
through Markdown
-* `{{ pages }}` - A collection of all the content pages in your site
- * `{{ page.id }}` - The relative path to the content file (unique ID)
- * `{{ page.url }}` - The URL to the page
- * `{{ page.title }}` - The title of the page (YAML header)
- * `{{ page.description }}` - The description of the page (YAML header)
- * `{{ page.author }}` - The author of the page (YAML header)
- * `{{ page.time }}` - The [Unix timestamp][UnixTimestamp] derived from
- the `Date` header
- * `{{ page.date }}` - The date of the page (YAML header)
- * `{{ page.date_formatted }}` - The formatted date of the page as specified
- by the `date_format` parameter in your
- `config/config.yml`
- * `{{ page.raw_content }}` - The raw, not yet parsed contents of the page;
- use Twig's `content` filter to get the parsed
- contents of a page by passing its unique ID
- (e.g. `{{ "sub/page"|content }}`)
- * `{{ page.meta }}`- The meta values of the page (see `{{ meta }}` above)
- * `{{ page.previous_page }}` - The data of the respective previous page
- * `{{ page.next_page }}` - The data of the respective next page
- * `{{ page.tree_node }}` - The page's node in Pico's page tree
-* `{{ prev_page }}` - The data of the previous page (relative to `current_page`)
-* `{{ current_page }}` - The data of the current page (see `{{ pages }}` above)
-* `{{ next_page }}` - The data of the next page (relative to `current_page`)
-
-Pages can be used like the following:
-
-
-
-Besides using the `{{ pages }}` list, you can also access pages using Pico's
-page tree. The page tree allows you to iterate through Pico's pages using a tree
-structure, so you can e.g. iterate just a page's direct children. It allows you
-to build recursive menus (like dropdowns) and to filter pages more easily. Just
-head over to Pico's [page tree documentation][FeaturesPageTree] for details.
+* `{{ previous_page }}` - The data of the previous page, relative to
+ `current_page`
+* `{{ current_page }}` - The data of the current page; refer to the "Pages"
+ section below for details
+* `{{ next_page }}` - The data of the next page, relative to `current_page`
To call assets from your theme, use `{{ theme_url }}`. For instance, to include
the CSS file `themes/my_theme/example.css`, add
@@ -251,19 +242,104 @@ the CSS file `themes/my_theme/example.css`, add
to your `index.twig`. This works for arbitrary files in your theme's folder,
including images and JavaScript files.
-Additional to Twigs extensive list of filters, functions and tags, Pico also
-provides some useful additional filters to make theming easier.
+Please note that Twig escapes HTML in all strings before outputting them. So
+for example, if you add `headline: My favorite color` to the
+YAML header of a page and output it using `{{ meta.headline }}`, you'll end up
+seeing `My favorite color` - yes, including the markup! To
+actually get it parsed, you must use `{{ meta.headline|raw }}` (resulting in
+the expected My **favorite** color). Notable exceptions to this
+are Pico's `content` variable (e.g. `{{ content }}`), Pico's `content` filter
+(e.g. `{{ "sub/page"|content }}`), and Pico's `markdown` filter, they all are
+marked as HTML safe.
+
+#### Dealing with pages
+
+There are several ways to access Pico's pages list. You can access the current
+page's data using the `current_page` variable, or use the `prev_page` and/or
+`next_page` variables to access the respective previous/next page in Pico's
+pages list. But more importantly there's the `pages()` function. No matter how
+you access a page, it will always consist of the following data:
+
+* `{{ id }}` - The relative path to the content file (unique ID)
+* `{{ url }}` - The URL to the page
+* `{{ title }}` - The title of the page (`Title` YAML header)
+* `{{ description }}` - The description of the page (`Description` YAML header)
+* `{{ author }}` - The author of the page (`Author` YAML header)
+* `{{ date }}` - The date of the page (`Date` YAML header)
+* `{{ date_formatted }}` - The formatted date of the page as specified by the
+ `date_format` parameter in your `config/config.yml`
+* `{{ time }}` - The [Unix timestamp][UnixTimestamp] derived from the page's
+ date
+* `{{ raw_content }}` - The raw, not yet parsed contents of the page; use the
+ filter to get the parsed contents of a page by passing
+ its unique ID (e.g. `{{ "sub/page"|content }}`)
+* `{{ meta }}` - The meta values of the page (see global `{{ meta }}` above)
+* `{{ prev_page }}` - The data of the respective previous page
+* `{{ next_page }}` - The data of the respective next page
+* `{{ tree_node }}` - The page's node in Pico's page tree; check out Pico's
+ [page tree documentation][FeaturesPageTree] for details
+
+Pico's `pages()` function is the best way to access all of your site's pages.
+It uses Pico's page tree to easily traverse a subset of Pico's pages list. It
+allows you to filter pages and to build recursive menus (like dropdowns). By
+default, `pages()` returns a list of all main pages (e.g. `content/page.md` and
+`content/sub/index.md`, but not `content/sub/page.md` or `content/index.md`).
+If you want to return all pages below a specific folder (e.g. `content/blog/`),
+pass the folder name as first parameter to the function (e.g. `pages("blog")`).
+Naturally you can also pass variables to the function. For example, to return a
+list of all child pages of the current page, use `pages(current_page.id)`.
+Check out the following code snippet:
+
+
+ {% for page in pages(current_page.id) if not page.hidden %}
+
+
+ {{ page.id|content }}
+
+ {% endfor %}
+
+
+The `pages()` function is very powerful and also allows you to return not just
+a page's child pages by passing the `depth` and `depthOffset` params. For
+example, if you pass `pages(depthOffset=-1)`, the list will also include Pico's
+main index page (i.e. `content/index.md`). This one is commonly used to create
+a theme's main navigation. If you want to learn more, head over to Pico's
+complete [`pages()` function documentation][FeaturesPagesFunction].
+
+If you want to access the data of a particular page, use Pico's `pages`
+variable. Just take `content/_meta.md` in Pico's sample contents for an
+example: `content/_meta.md` contains some meta data you might want to use in
+your theme. If you want to output the page's `tagline` meta value, use
+`{{ pages["_meta"].meta.logo }}`. Don't ever try to use Pico's `pages` variable
+as an replacement for Pico's `pages()` function. Its usage looks very similar,
+it will kinda work and you might even see it being used in old themes, but be
+warned: It slows down Pico. Always use Pico's `pages()` function when iterating
+Pico's page list (e.g. `{% for page in pages() %}…{% endfor %}`).
+
+#### Twig filters and functions
+
+Additional to [Twig][]'s extensive list of filters, functions and tags, Pico
+also provides some useful additional filters and functions to make theming
+even easier.
* Pass the unique ID of a page to the `link` filter to return the page's URL
(e.g. `{{ "sub/page"|link }}` gets `%base_url%?sub/page`).
+* You can replace URL placeholders (like %base_url%) in
+ arbitrary strings using the `url` filter. This is helpful together with meta
+ variables, e.g. if you add image: %assets_url%/stock.jpg
+ to the YAML header of a page, `{{ meta.image|url }}` will return
+ `%assets_url%/stock.jpg`.
* To get the parsed contents of a page, pass its unique ID to the `content`
filter (e.g. `{{ "sub/page"|content }}`).
-* You can parse any Markdown string using the `markdown` filter (e.g. you can
- use Markdown in the `description` meta variable and later parse it in your
- theme using `{{ meta.description|markdown }}`). You can pass meta data as
- parameter to replace %meta.*% placeholders (e.g.
- `{{ "Written *by %meta.author%*"|markdown(meta) }}` yields "Written by
- *John Doe*").
+* You can parse any Markdown string using the `markdown` filter. For example,
+ you might use Markdown in the `description` meta variable and later parse it
+ in your theme using `{{ meta.description|markdown }}`. You can also pass meta
+ data as parameter to replace %meta.*% placeholders
+ (e.g. `{{ "Written by *%meta.author%*"|markdown(meta) }}` yields "Written by
+ *John Doe*"). However, please note that all contents will be wrapped inside
+ HTML paragraph elements (i.e. `
…
`). If you want to parse just a single
+ line of Markdown markup, pass the `singleLine` param to the `markdown` filter
+ (e.g. `{{ "This really is a *single* line"|markdown(singleLine=true) }}`).
* Arrays can be sorted by one of its keys using the `sort_by` filter
(e.g. `{% for page in pages|sort_by([ 'meta', 'nav' ]) %}...{% endfor %}`
iterates through all pages, ordered by the `nav` meta header; please note the
@@ -281,18 +357,6 @@ provides some useful additional filters to make theming easier.
Twig! Simply head over to our [introductory page for accessing HTTP
parameters][FeaturesHttpParams] for details.
-You can use different templates for different content files by specifying the
-`Template` meta header. Simply add e.g. `Template: blog` to the YAML header of
-a content file and Pico will use the `blog.twig` template in your theme folder
-to display the page.
-
-Pico's default theme isn't really intended to be used for a productive website,
-it's rather a starting point for creating your own theme. If the default theme
-isn't sufficient for you, and you don't want to create your own theme, you can
-use one of the great themes third-party developers and designers created in the
-past. As with plugins, you can find themes in [our Wiki][WikiThemes] and on
-[our website][OfficialThemes].
-
### Plugins
#### Plugins for users
@@ -425,6 +489,7 @@ url.rewrite-if-not-file = (
For more help have a look at the Pico documentation at http://picocms.org/docs.
[Pico]: http://picocms.org/
+[PicoTheme]: https://github.com/picocms/pico-theme
[SampleContents]: https://github.com/picocms/Pico/tree/master/content-sample
[Markdown]: http://daringfireball.net/projects/markdown/syntax
[MarkdownExtra]: https://michelf.ca/projects/php-markdown/extra/
@@ -434,6 +499,7 @@ For more help have a look at the Pico documentation at http://picocms.org/docs.
[Composer]: https://getcomposer.org/
[FeaturesHttpParams]: http://picocms.org/in-depth/features/http-params/
[FeaturesPageTree]: http://picocms.org/in-depth/features/page-tree/
+[FeaturesPagesFunction]: http://picocms.org/in-depth/features/pages-function/
[WikiThemes]: https://github.com/picocms/Pico/wiki/Pico-Themes
[WikiPlugins]: https://github.com/picocms/Pico/wiki/Pico-Plugins
[OfficialThemes]: http://picocms.org/themes/
diff --git a/content-sample/theme.md b/content-sample/theme.md
new file mode 100644
index 0000000..a11aaa1
--- /dev/null
+++ b/content-sample/theme.md
@@ -0,0 +1,195 @@
+---
+title: Theme Styling Test
+hidden: true
+---
+
+Theme Styling Test
+==================
+
+This is `theme.md` in Pico's content directory. This page doesn't show up in the website's menu due to `hidden: true` in the page's YAML header. The purpose of this page is to aid theme development - below you'll find basically every format that is possible with Markdown. If you develop a theme, you should make sure that all examples below show decent.
+
+Text
+----
+
+**Lorem ipsum dolor sit amet,** consectetur adipisici elit, *sed eiusmod tempor* incidunt ut labore et dolore magna aliqua.[^1] ~~Ut enim ad minim veniam,~~ quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.[^2] [Quis aute iure reprehenderit][Link] in voluptate velit esse cillum dolore eu fugiat nulla pariatur. `Excepteur` sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+[](%base_url% "Pico Logo") {.image .small .float-right}
+
+Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+
+Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
+
+Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
+
+Headings
+--------
+
+# h1
+
+Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+## h2
+
+Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+
+### h3
+
+Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
+
+#### h4
+
+Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
+
+##### h5
+
+Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.
+
+###### h6
+
+At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+
+Horizontal line
+---------------
+
+Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+---
+
+Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+
+List
+----
+
+Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+* Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
+ 1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+ 2. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
+ 3. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
+* Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.
+ - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+ - Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
+ 1. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.
+ 2. At vero eos et accusam et justo duo dolores et ea rebum.
+ 1. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet
+ 2. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
+ 3. At vero eos et accusam et justo duo dolores et ea rebum.
+
+Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.
+
+Definition list
+---------------
+
+Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+Duis autem
+: Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
+
+Lorem ipsum dolor sit amet
+: Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+
+Ut wisi enim ad minim veniam
+: Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
+: Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
+
+Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
+
+Blockquote
+----------
+
+Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+> Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
+> molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero
+> eros et accumsan et iusto odio dignissim qui blandit praesent luptatum
+> zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum
+> dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod
+> tincidunt ut laoreet dolore magna aliquam erat volutpat.
+>
+> > Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit
+> > lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure
+> > dolor in hendrerit in vulputate velit esse molestie consequat, vel illum
+> > dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio
+> > dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te
+> > feugait nulla facilisi.
+>
+> Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet
+> doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet,
+> consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut
+> laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam,
+> quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex
+> ea commodo consequat.
+
+Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.
+
+Code block
+----------
+
+Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+```
+
+
+
+ This is a title
+
+
+
Hello world!
+
+
+```
+
+Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+
+Table
+-----
+
+Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+Lorem ipsum | Duis autem vel eum | Ut wisi enim ad minim veniam
+----------- | ------------------ | ----------------------------
+**Duis autem vel eum iriure dolor** in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. | *Lorem ipsum dolor sit amet,* consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. | ~~Ut wisi enim ad minim veniam,~~ quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
+[Duis autem vel eum iriure dolor][Link] in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. | `Nam liber tempor` cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. | Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. | | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.
+
+At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+
+Forms
+-----
+
+Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+
+*[Lorem ipsum]: Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.
+
+[Link]: %base_url% "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat."
+
+[^1]: Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+[^2]: Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
diff --git a/index.php b/index.php
index 45ecdd2..0d13d6d 100644
--- a/index.php
+++ b/index.php
@@ -18,7 +18,11 @@ if (is_file(__DIR__ . '/vendor/autoload.php')) {
// composer dependency package
require_once(__DIR__ . '/../../../vendor/autoload.php');
} else {
- die("Cannot find 'vendor/autoload.php'. Run `composer install`.");
+ die(
+ "Cannot find 'vendor/autoload.php'. If you're using a composer-based Pico install, run `composer install`. "
+ . "If you're rather trying to use one of Pico's pre-built release packages, make sure to download Pico's "
+ . "latest release package named 'pico-release-v*.tar.gz' (don't download a source code package)."
+ );
}
// instance Pico
diff --git a/lib/AbstractPicoPlugin.php b/lib/AbstractPicoPlugin.php
index 41f954f..b732ac2 100644
--- a/lib/AbstractPicoPlugin.php
+++ b/lib/AbstractPicoPlugin.php
@@ -21,7 +21,7 @@
* @author Daniel Rudolf
* @link http://picocms.org
* @license http://opensource.org/licenses/MIT The MIT License
- * @version 2.0
+ * @version 2.1
*/
abstract class AbstractPicoPlugin implements PicoPluginInterface
{
@@ -31,7 +31,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
* @see PicoPluginInterface::getPico()
* @var Pico
*/
- private $pico;
+ protected $pico;
/**
* Boolean indicating if this plugin is enabled (TRUE) or disabled (FALSE)
@@ -74,10 +74,12 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
* @see PicoPluginInterface::getDependants()
* @var object[]|null
*/
- private $dependants;
+ protected $dependants;
/**
- * @see PicoPluginInterface::__construct()
+ * Constructs a new instance of a Pico plugin
+ *
+ * @param Pico $pico current instance of Pico
*/
public function __construct(Pico $pico)
{
@@ -85,31 +87,13 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
}
/**
- * @see PicoPluginInterface::handleEvent()
+ * {@inheritDoc}
*/
public function handleEvent($eventName, array $params)
{
// plugins can be enabled/disabled using the config
if ($eventName === 'onConfigLoaded') {
- $pluginEnabled = $this->getConfig(get_called_class() . '.enabled');
- if ($pluginEnabled !== null) {
- $this->setEnabled($pluginEnabled);
- } else {
- $pluginEnabled = $this->getPluginConfig('enabled');
- if ($pluginEnabled !== null) {
- $this->setEnabled($pluginEnabled);
- } elseif ($this->enabled) {
- $this->setEnabled($this->enabled, true, true);
- } elseif ($this->enabled === null) {
- // make sure dependencies are already fulfilled,
- // otherwise the plugin needs to be enabled manually
- try {
- $this->setEnabled(true, false, true);
- } catch (RuntimeException $e) {
- $this->enabled = false;
- }
- }
- }
+ $this->configEnabled();
}
if ($this->isEnabled() || ($eventName === 'onPluginsLoaded')) {
@@ -120,7 +104,33 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
}
/**
- * @see PicoPluginInterface::setEnabled()
+ * Enables or disables this plugin depending on Pico's config
+ */
+ protected function configEnabled()
+ {
+ $pluginEnabled = $this->getPico()->getConfig(get_called_class() . '.enabled');
+ if ($pluginEnabled !== null) {
+ $this->setEnabled($pluginEnabled);
+ } else {
+ $pluginEnabled = $this->getPluginConfig('enabled');
+ if ($pluginEnabled !== null) {
+ $this->setEnabled($pluginEnabled);
+ } elseif ($this->enabled) {
+ $this->setEnabled(true, true, true);
+ } elseif ($this->enabled === null) {
+ // make sure dependencies are already fulfilled,
+ // otherwise the plugin needs to be enabled manually
+ try {
+ $this->setEnabled(true, false, true);
+ } catch (RuntimeException $e) {
+ $this->enabled = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
*/
public function setEnabled($enabled, $recursive = true, $auto = false)
{
@@ -136,7 +146,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
}
/**
- * @see PicoPluginInterface::isEnabled()
+ * {@inheritDoc}
*/
public function isEnabled()
{
@@ -144,7 +154,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
}
/**
- * @see PicoPluginInterface::isStatusChanged()
+ * {@inheritDoc}
*/
public function isStatusChanged()
{
@@ -152,7 +162,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
}
/**
- * @see PicoPluginInterface::getPico()
+ * {@inheritDoc}
*/
public function getPico()
{
@@ -174,7 +184,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
*/
public function getPluginConfig($configName = null, $default = null)
{
- $pluginConfig = $this->getConfig(get_called_class(), array());
+ $pluginConfig = $this->getPico()->getConfig(get_called_class(), array());
if ($configName === null) {
return $pluginConfig;
@@ -186,7 +196,9 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
/**
* Passes all not satisfiable method calls to Pico
*
- * @see Pico
+ * @see PicoPluginInterface::getPico()
+ *
+ * @deprecated 2.1.0
*
* @param string $methodName name of the method to call
* @param array $params parameters to pass
@@ -212,15 +224,13 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
*
* @param bool $recursive enable required plugins automatically
*
- * @return void
- *
* @throws RuntimeException thrown when a dependency fails
*/
protected function checkDependencies($recursive)
{
foreach ($this->getDependencies() as $pluginName) {
try {
- $plugin = $this->getPlugin($pluginName);
+ $plugin = $this->getPico()->getPlugin($pluginName);
} catch (RuntimeException $e) {
throw new RuntimeException(
"Unable to enable plugin '" . get_called_class() . "': "
@@ -250,7 +260,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
}
/**
- * @see PicoPluginInterface::getDependencies()
+ * {@inheritDoc}
*/
public function getDependencies()
{
@@ -264,8 +274,6 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
*
* @param bool $recursive disabled dependant plugins automatically
*
- * @return void
- *
* @throws RuntimeException thrown when a dependency fails
*/
protected function checkDependants($recursive)
@@ -297,13 +305,13 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
}
/**
- * @see PicoPluginInterface::getDependants()
+ * {@inheritDoc}
*/
public function getDependants()
{
if ($this->dependants === null) {
$this->dependants = array();
- foreach ($this->getPlugins() as $pluginName => $plugin) {
+ foreach ($this->getPico()->getPlugins() as $pluginName => $plugin) {
// only plugins which implement PicoPluginInterface support dependencies
if ($plugin instanceof PicoPluginInterface) {
$dependencies = $plugin->getDependencies();
@@ -322,13 +330,11 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
*
* Pico automatically adds a dependency to {@see PicoDeprecated} when the
* plugin's API is older than Pico's API. {@see PicoDeprecated} furthermore
- * throws a exception when it can't provide compatibility in such cases.
+ * throws a exception if it can't provide compatibility in such cases.
* However, we still have to decide whether this plugin is compatible to
* newer API versions, what requires some special (version specific)
* precaution and is therefore usually not the case.
*
- * @return void
- *
* @throws RuntimeException thrown when the plugin's and Pico's API aren't
* compatible
*/
diff --git a/lib/Pico.php b/lib/Pico.php
index 8cbc4b1..7b32854 100644
--- a/lib/Pico.php
+++ b/lib/Pico.php
@@ -40,7 +40,7 @@
* @author Daniel Rudolf
* @link http://picocms.org
* @license http://opensource.org/licenses/MIT The MIT License
- * @version 2.0
+ * @version 2.1
*/
class Pico
{
@@ -49,21 +49,21 @@ class Pico
*
* @var string
*/
- const VERSION = '2.0.5-beta.1';
+ const VERSION = '2.1.0';
/**
* Pico version ID
*
* @var int
*/
- const VERSION_ID = 20005;
+ const VERSION_ID = 20100;
/**
* Pico API version
*
* @var int
*/
- const API_VERSION = 2;
+ const API_VERSION = 3;
/**
* Sort files in alphabetical ascending order
@@ -168,6 +168,29 @@ class Pico
*/
protected $config;
+ /**
+ * Theme in use
+ *
+ * @see Pico::getTheme()
+ * @var string
+ */
+ protected $theme;
+
+ /**
+ * API version of the current theme
+ *
+ * @see Pico::getThemeApiVersion()
+ * @var int
+ */
+ protected $themeApiVersion;
+
+ /**
+ * Additional meta headers of the current theme
+ *
+ * @var array|null
+ */
+ protected $themeMetaHeaders;
+
/**
* Part of the URL describing the requested contents
*
@@ -212,7 +235,7 @@ class Pico
* List of known meta headers
*
* @see Pico::getMetaHeaders()
- * @var string[]|null
+ * @var array|null
*/
protected $metaHeaders;
@@ -411,6 +434,16 @@ class Pico
throw new RuntimeException('Invalid content directory "' . $this->getConfig('content_dir') . '"');
}
+ // load theme
+ $this->theme = $this->config['theme'];
+ $this->triggerEvent('onThemeLoading', array(&$this->theme));
+
+ $this->loadTheme();
+ $this->triggerEvent(
+ 'onThemeLoaded',
+ array($this->theme, $this->themeApiVersion, &$this->config['theme_config'])
+ );
+
// evaluate request url
$this->evaluateRequestUrl();
$this->triggerEvent('onRequestUrl', array(&$this->requestUrl));
@@ -423,7 +456,7 @@ class Pico
$this->triggerEvent('onContentLoading');
$hiddenFileRegex = '/(?:^|\/)(?:_|404' . preg_quote($this->getConfig('content_ext'), '/') . '$)/';
- if (file_exists($this->requestFile) && !preg_match($hiddenFileRegex, $this->requestFile)) {
+ if (is_file($this->requestFile) && !preg_match($hiddenFileRegex, $this->requestFile)) {
$this->rawContent = $this->loadFileContent($this->requestFile);
} else {
$this->triggerEvent('on404ContentLoading');
@@ -508,8 +541,6 @@ class Pico
* @see Pico::getPlugin()
* @see Pico::getPlugins()
*
- * @return void
- *
* @throws RuntimeException thrown when a plugin couldn't be loaded
*/
protected function loadPlugins()
@@ -541,14 +572,16 @@ class Pico
* @param string[] $pluginBlacklist class names of plugins not to load
*
* @return string[] installer names of the loaded plugins
+ *
+ * @throws RuntimeException thrown when a plugin couldn't be loaded
*/
protected function loadComposerPlugins(array $pluginBlacklist = array())
{
$composerPlugins = array();
- if (file_exists($this->getVendorDir() . 'vendor/pico-plugin.php')) {
+ if (is_file($this->getVendorDir() . 'vendor/pico-plugin.php')) {
// composer root package
$composerPlugins = require($this->getVendorDir() . 'vendor/pico-plugin.php') ?: array();
- } elseif (file_exists($this->getVendorDir() . '../../../vendor/pico-plugin.php')) {
+ } elseif (is_file($this->getVendorDir() . '../../../vendor/pico-plugin.php')) {
// composer dependency package
$composerPlugins = require($this->getVendorDir() . '../../../vendor/pico-plugin.php') ?: array();
}
@@ -608,8 +641,6 @@ class Pico
*
* @param string[] $pluginBlacklist class names of plugins not to load
*
- * @return void
- *
* @throws RuntimeException thrown when a plugin couldn't be loaded
*/
protected function loadLocalPlugins(array $pluginBlacklist = array())
@@ -635,7 +666,7 @@ class Pico
$className = preg_replace('/^[0-9]+-/', '', $file);
$pluginFile = $file . '/' . $className . '.php';
- if (!file_exists($this->getPluginsDir() . $pluginFile)) {
+ if (!is_file($this->getPluginsDir() . $pluginFile)) {
throw new RuntimeException(
"Unable to load plugin '" . $className . "' from '" . $pluginFile . "': File not found"
);
@@ -703,7 +734,7 @@ class Pico
*
* @return PicoPluginInterface instance of the loaded plugin
*
- * @throws RuntimeException thrown when a plugin couldn't be loaded
+ * @throws RuntimeException thrown when the plugin couldn't be loaded
*/
public function loadPlugin($plugin)
{
@@ -712,7 +743,7 @@ class Pico
if (class_exists($className)) {
$plugin = new $className($this);
} else {
- throw new RuntimeException("Unable to load plugin '" . $className . "': Class not found");
+ throw new RuntimeException("Unable to load plugin '" . $className . "': Class not found");
}
}
@@ -764,8 +795,6 @@ class Pico
* Marc J. Schmidt's Topological Sort / Dependency resolver in PHP
* @see https://github.com/marcj/topsort.php/blob/1.1.0/src/Implementations/ArraySort.php
* \MJS\TopSort\Implementations\ArraySort class
- *
- * @return void
*/
protected function sortPlugins()
{
@@ -862,7 +891,7 @@ class Pico
}
/**
- * Loads the config.yml and any other *.yml from Pico::$configDir
+ * Loads config.yml and any other *.yml from Pico::$configDir
*
* After loading {@path "config/config.yml"}, Pico proceeds with any other
* existing `config/*.yml` file in alphabetical order. The file order is
@@ -874,8 +903,6 @@ class Pico
*
* @see Pico::setConfig()
* @see Pico::getConfig()
- *
- * @return void
*/
protected function loadConfig()
{
@@ -889,7 +916,7 @@ class Pico
// load main config file (config/config.yml)
$this->config = is_array($this->config) ? $this->config : array();
- if (file_exists($this->getConfigDir() . 'config.yml')) {
+ if (is_file($this->getConfigDir() . 'config.yml')) {
$this->config += $loadConfigClosure($this->getConfigDir() . 'config.yml');
}
@@ -904,18 +931,25 @@ class Pico
// merge default config
$this->config += array(
'site_title' => 'Pico',
- 'base_url' => '',
+ 'base_url' => null,
'rewrite_url' => null,
+ 'debug' => null,
'timezone' => null,
'theme' => 'default',
- 'theme_url' => null,
+ 'theme_config' => null,
+ 'theme_meta' => null,
+ 'themes_url' => null,
'twig_config' => null,
'date_format' => '%D %T',
+ 'pages_order_by_meta' => 'author',
'pages_order_by' => 'alpha',
'pages_order' => 'asc',
'content_dir' => null,
'content_ext' => '.md',
- 'content_config' => null
+ 'content_config' => null,
+ 'assets_dir' => 'assets/',
+ 'assets_url' => null,
+ 'plugins_url' => null
);
if (!$this->config['base_url']) {
@@ -928,6 +962,10 @@ class Pico
$this->config['rewrite_url'] = $this->isUrlRewritingEnabled();
}
+ if ($this->config['debug'] === null) {
+ $this->config['debug'] = $this->isDebugModeEnabled();
+ }
+
if (!$this->config['timezone']) {
// explicitly set a default timezone to prevent a E_NOTICE when no timezone is set;
// the `date_default_timezone_get()` function always returns a timezone, at least UTC
@@ -935,19 +973,16 @@ class Pico
}
date_default_timezone_set($this->config['timezone']);
- if (!$this->config['theme_url']) {
- $this->config['theme_url'] = $this->getBaseThemeUrl();
- } elseif (preg_match('#^[A-Za-z][A-Za-z0-9+\-.]*://#', $this->config['theme_url'])) {
- $this->config['theme_url'] = rtrim($this->config['theme_url'], '/') . '/';
+ if (!$this->config['plugins_url']) {
+ $this->config['plugins_url'] = $this->getUrlFromPath($this->getPluginsDir());
} else {
- $this->config['theme_url'] = $this->getBaseUrl() . rtrim($this->config['theme_url'], '/') . '/';
+ $this->config['plugins_url'] = $this->getAbsoluteUrl($this->config['plugins_url']);
}
- $defaultTwigConfig = array('cache' => false, 'autoescape' => false, 'debug' => false);
- if (!is_array($this->config['twig_config'])) {
- $this->config['twig_config'] = $defaultTwigConfig;
+ if (!$this->config['themes_url']) {
+ $this->config['themes_url'] = $this->getUrlFromPath($this->getThemesDir());
} else {
- $this->config['twig_config'] += $defaultTwigConfig;
+ $this->config['themes_url'] = $this->getAbsoluteUrl($this->config['themes_url']);
}
if (!$this->config['content_dir']) {
@@ -969,6 +1004,18 @@ class Pico
} else {
$this->config['content_config'] += $defaultContentConfig;
}
+
+ if (!$this->config['assets_dir']) {
+ $this->config['assets_dir'] = $this->getRootDir() . 'assets/';
+ } else {
+ $this->config['assets_dir'] = $this->getAbsolutePath($this->config['assets_dir']);
+ }
+
+ if (!$this->config['assets_url']) {
+ $this->config['assets_url'] = $this->getUrlFromPath($this->config['assets_dir']);
+ } else {
+ $this->config['assets_url'] = $this->getAbsoluteUrl($this->config['assets_url']);
+ }
}
/**
@@ -987,8 +1034,6 @@ class Pico
*
* @param array $config array with config variables
*
- * @return void
- *
* @throws LogicException thrown if Pico already started processing
*/
public function setConfig(array $config)
@@ -1025,6 +1070,109 @@ class Pico
}
}
+ /**
+ * Loads a theme's config file (pico-theme.yml)
+ *
+ * @see Pico::getTheme()
+ * @see Pico::getThemeApiVersion()
+ */
+ protected function loadTheme()
+ {
+ $themeConfig = array();
+
+ // load theme config from pico-theme.yml
+ $themeConfigFile = $this->getThemesDir() . $this->getTheme() . '/pico-theme.yml';
+ if (is_file($themeConfigFile)) {
+ $themeConfigYaml = file_get_contents($themeConfigFile);
+ $themeConfig = $this->getYamlParser()->parse($themeConfigYaml);
+ $themeConfig = is_array($themeConfig) ? $themeConfig : array();
+ }
+
+ $themeConfig += array(
+ 'api_version' => null,
+ 'meta' => array(),
+ 'twig_config' => array()
+ );
+
+ // theme API version
+ if (is_int($themeConfig['api_version']) || preg_match('/^[0-9]+$/', $themeConfig['api_version'])) {
+ $this->themeApiVersion = (int) $themeConfig['api_version'];
+ } else {
+ $this->themeApiVersion = 0;
+ }
+
+ unset($themeConfig['api_version']);
+
+ // twig config
+ $themeTwigConfig = array('autoescape' => 'html', 'strict_variables' => false, 'charset' => 'utf-8');
+ foreach ($themeTwigConfig as $key => $_) {
+ if (isset($themeConfig['twig_config'][$key])) {
+ $themeTwigConfig[$key] = $themeConfig['twig_config'][$key];
+ }
+ }
+
+ unset($themeConfig['twig_config']);
+
+ $defaultTwigConfig = array('debug' => null, 'cache' => false, 'auto_reload' => null);
+ $this->config['twig_config'] = is_array($this->config['twig_config']) ? $this->config['twig_config'] : array();
+ $this->config['twig_config'] = array_merge($defaultTwigConfig, $themeTwigConfig, $this->config['twig_config']);
+
+ if ($this->config['twig_config']['autoescape'] === true) {
+ $this->config['twig_config']['autoescape'] = 'html';
+ }
+ if ($this->config['twig_config']['cache']) {
+ $this->config['twig_config']['cache'] = $this->getAbsolutePath($this->config['twig_config']['cache']);
+ }
+ if ($this->config['twig_config']['debug'] === null) {
+ $this->config['twig_config']['debug'] = $this->isDebugModeEnabled();
+ }
+
+ // meta headers
+ $this->themeMetaHeaders = is_array($themeConfig['meta']) ? $themeConfig['meta'] : array();
+ unset($themeConfig['meta']);
+
+ // theme config
+ if (!is_array($this->config['theme_config'])) {
+ $this->config['theme_config'] = $themeConfig;
+ } else {
+ $this->config['theme_config'] += $themeConfig;
+ }
+
+ // check for theme compatibility
+ if (!isset($this->plugins['PicoDeprecated']) && ($this->themeApiVersion < static::API_VERSION)) {
+ throw new RuntimeException(
+ 'Current theme "' . $this->theme . '" uses API version ' . $this->themeApiVersion . ', but Pico '
+ . 'provides API version ' . static::API_VERSION . ' and PicoDeprecated isn\'t loaded'
+ );
+ }
+ }
+
+ /**
+ * Returns the name of the current theme
+ *
+ * @see Pico::loadTheme()
+ * @see Pico::getThemeApiVersion()
+ *
+ * @return string
+ */
+ public function getTheme()
+ {
+ return $this->theme;
+ }
+
+ /**
+ * Returns the API version of the current theme
+ *
+ * @see Pico::loadTheme()
+ * @see Pico::getTheme()
+ *
+ * @return int
+ */
+ public function getThemeApiVersion()
+ {
+ return $this->themeApiVersion;
+ }
+
/**
* Evaluates the requested URL
*
@@ -1061,8 +1209,6 @@ class Pico
* `/pico/?someBooleanParam=` or `/pico/?index&someBooleanParam` instead.
*
* @see Pico::getRequestUrl()
- *
- * @return void
*/
protected function evaluateRequestUrl()
{
@@ -1136,39 +1282,22 @@ class Pico
if (!$requestUrl) {
return $contentDir . 'index' . $contentExt;
} else {
- // prevent content_dir breakouts
- $requestUrl = str_replace('\\', '/', $requestUrl);
- $requestUrlParts = explode('/', $requestUrl);
-
- $requestFileParts = array();
- foreach ($requestUrlParts as $requestUrlPart) {
- if (($requestUrlPart === '') || ($requestUrlPart === '.')) {
- continue;
- } elseif ($requestUrlPart === '..') {
- array_pop($requestFileParts);
- continue;
- }
-
- $requestFileParts[] = $requestUrlPart;
- }
-
- if (!$requestFileParts) {
- return $contentDir . 'index' . $contentExt;
- }
+ // normalize path and prevent content_dir breakouts
+ $requestFile = $this->getNormalizedPath($requestUrl, false, false);
// discover the content file to serve
- // Note: $requestFileParts neither contains a trailing nor a leading slash
- $requestFile = $contentDir . implode('/', $requestFileParts);
- if (is_dir($requestFile)) {
+ if (!$requestFile) {
+ return $contentDir . 'index' . $contentExt;
+ } elseif (is_dir($contentDir . $requestFile)) {
// if no index file is found, try a accordingly named file in the previous dir
// if this file doesn't exist either, show the 404 page, but assume the index
// file as being requested (maintains backward compatibility to Pico < 1.0)
- $indexFile = $requestFile . '/index' . $contentExt;
- if (file_exists($indexFile) || !file_exists($requestFile . $contentExt)) {
+ $indexFile = $contentDir . $requestFile . '/index' . $contentExt;
+ if (is_file($indexFile) || !is_file($contentDir . $requestFile . $contentExt)) {
return $indexFile;
}
}
- return $requestFile . $contentExt;
+ return $contentDir . $requestFile . $contentExt;
}
}
@@ -1223,11 +1352,11 @@ class Pico
$errorFileDir = dirname($errorFileDir);
$errorFile = $errorFileDir . '/404' . $contentExt;
- if (file_exists($contentDir . $errorFile)) {
+ if (is_file($contentDir . $errorFile)) {
return $this->loadFileContent($contentDir . $errorFile);
}
}
- } elseif (file_exists($contentDir . '404' . $contentExt)) {
+ } elseif (is_file($contentDir . '404' . $contentExt)) {
// provided that the requested file is not in the regular
// content directory, fallback to Pico's global `404.md`
return $this->loadFileContent($contentDir . '404' . $contentExt);
@@ -1293,6 +1422,10 @@ class Pico
'Hidden' => 'hidden'
);
+ if ($this->themeMetaHeaders) {
+ $this->metaHeaders += $this->themeMetaHeaders;
+ }
+
$this->triggerEvent('onMetaHeaders', array(&$this->metaHeaders));
}
@@ -1340,7 +1473,7 @@ class Pico
public function parseFileMeta($rawContent, array $headers)
{
$meta = array();
- $pattern = "/^(\/(\*)|---)[[:blank:]]*(?:\r)?\n"
+ $pattern = "/^(?:\xEF\xBB\xBF)?(\/(\*)|---)[[:blank:]]*(?:\r)?\n"
. "(?:(.*?)(?:\r)?\n)?(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s";
if (preg_match($pattern, $rawContent, $rawMetaMatches) && isset($rawMetaMatches[3])) {
$meta = $this->getYamlParser()->parse($rawMetaMatches[3]) ?: array();
@@ -1444,7 +1577,7 @@ class Pico
public function prepareFileContent($rawContent, array $meta = array())
{
// remove meta header
- $metaHeaderPattern = "/^(\/(\*)|---)[[:blank:]]*(?:\r)?\n"
+ $metaHeaderPattern = "/^(?:\xEF\xBB\xBF)?(\/(\*)|---)[[:blank:]]*(?:\r)?\n"
. "(?:(.*?)(?:\r)?\n)?(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s";
$markdown = preg_replace($metaHeaderPattern, '', $rawContent, 1);
@@ -1483,8 +1616,13 @@ class Pico
}
$variables['%base_url%'] = rtrim($this->getBaseUrl(), '/');
+ // replace %plugins_url%, %themes_url% and %assets_url%
+ $variables['%plugins_url%'] = rtrim($this->getConfig('plugins_url'), '/');
+ $variables['%themes_url%'] = rtrim($this->getConfig('themes_url'), '/');
+ $variables['%assets_url%'] = rtrim($this->getConfig('assets_url'), '/');
+
// replace %theme_url%
- $variables['%theme_url%'] = $this->getBaseThemeUrl() . $this->getConfig('theme');
+ $variables['%theme_url%'] = $this->getConfig('themes_url') . $this->getTheme();
// replace %meta.*%
if ($meta) {
@@ -1495,6 +1633,13 @@ class Pico
}
}
+ // replace %config.*%
+ foreach ($this->config as $configKey => $configValue) {
+ if (is_scalar($configValue) || ($configValue === null)) {
+ $variables['%config.' . $configKey . '%'] = (string) $configValue;
+ }
+ }
+
return str_replace(array_keys($variables), $variables, $markdown);
}
@@ -1505,13 +1650,15 @@ class Pico
* @see Pico::substituteFileContent()
* @see Pico::getFileContent()
*
- * @param string $markdown Markdown contents of a page
+ * @param string $markdown Markdown contents of a page
+ * @param bool $singleLine whether to parse just a single line of markup
*
* @return string parsed contents (HTML)
*/
- public function parseFileContent($markdown)
+ public function parseFileContent($markdown, $singleLine = false)
{
- return $this->getParsedown()->text($markdown);
+ $markdownParser = $this->getParsedown();
+ return !$singleLine ? $markdownParser->text($markdown) : $markdownParser->line($markdown);
}
/**
@@ -1559,8 +1706,6 @@ class Pico
* @see Pico::sortPages()
* @see Pico::discoverPageSiblings()
* @see Pico::getPages()
- *
- * @return void
*/
protected function readPages()
{
@@ -1619,7 +1764,7 @@ class Pico
'time' => &$meta['time'],
'date' => &$meta['date'],
'date_formatted' => &$meta['date_formatted'],
- 'hidden' => (preg_match('/(?:^|\/)_/', $id) || $meta['hidden']),
+ 'hidden' => ($meta['hidden'] || preg_match('/(?:^|\/)_/', $id)),
'raw_content' => &$rawContent,
'meta' => &$meta
);
@@ -1644,8 +1789,6 @@ class Pico
*
* @see Pico::readPages()
* @see Pico::getPages()
- *
- * @return void
*/
protected function sortPages()
{
@@ -1725,8 +1868,6 @@ class Pico
*
* @see Pico::readPages()
* @see Pico::getPages()
- *
- * @return void
*/
protected function discoverPageSiblings()
{
@@ -1776,8 +1917,6 @@ class Pico
* @see Pico::getCurrentPage()
* @see Pico::getPreviousPage()
* @see Pico::getNextPage()
- *
- * @return void
*/
protected function discoverCurrentPage()
{
@@ -1864,8 +2003,6 @@ class Pico
* non-iterable data structure with Pico 3.0.
*
* @see Pico::getPageTree()
- *
- * @return void
*/
protected function buildPageTree()
{
@@ -1950,7 +2087,7 @@ class Pico
if ($this->twig === null) {
$twigConfig = $this->getConfig('twig_config');
- $twigLoader = new Twig_Loader_Filesystem($this->getThemesDir() . $this->getConfig('theme'));
+ $twigLoader = new Twig_Loader_Filesystem($this->getThemesDir() . $this->getTheme());
$this->twig = new Twig_Environment($twigLoader, $twigConfig);
$this->twig->addExtension(new PicoTwigExtension($this));
@@ -1963,17 +2100,21 @@ class Pico
// this is the reason why we can't register this filter as part of PicoTwigExtension
$pico = $this;
$pages = &$this->pages;
- $this->twig->addFilter(new Twig_SimpleFilter('content', function ($page) use ($pico, &$pages) {
- if (isset($pages[$page])) {
- $pageData = &$pages[$page];
- if (!isset($pageData['content'])) {
- $pageData['content'] = $pico->prepareFileContent($pageData['raw_content'], $pageData['meta']);
- $pageData['content'] = $pico->parseFileContent($pageData['content']);
+ $this->twig->addFilter(new Twig_SimpleFilter(
+ 'content',
+ function ($page) use ($pico, &$pages) {
+ if (isset($pages[$page])) {
+ $pageData = &$pages[$page];
+ if (!isset($pageData['content'])) {
+ $markdown = $pico->prepareFileContent($pageData['raw_content'], $pageData['meta']);
+ $pageData['content'] = $pico->parseFileContent($markdown);
+ }
+ return $pageData['content'];
}
- return $pageData['content'];
- }
- return null;
- }));
+ return null;
+ },
+ array('is_safe' => array('html'))
+ ));
// trigger onTwigRegistration event
$this->triggerEvent('onTwigRegistered', array(&$this->twig));
@@ -1985,8 +2126,7 @@ class Pico
/**
* Returns the variables passed to the template
*
- * URLs and paths (namely `base_dir`, `base_url`, `theme_dir` and
- * `theme_url`) don't add a trailing slash for historic reasons.
+ * URLs and paths don't add a trailing slash for historic reasons.
*
* @return array template variables
*/
@@ -1994,15 +2134,16 @@ class Pico
{
return array(
'config' => $this->getConfig(),
- 'base_dir' => rtrim($this->getRootDir(), '/'),
'base_url' => rtrim($this->getBaseUrl(), '/'),
- 'theme_dir' => $this->getThemesDir() . $this->getConfig('theme'),
- 'theme_url' => $this->getBaseThemeUrl() . $this->getConfig('theme'),
+ 'plugins_url' => rtrim($this->getConfig('plugins_url'), '/'),
+ 'themes_url' => rtrim($this->getConfig('themes_url'), '/'),
+ 'assets_url' => rtrim($this->getConfig('assets_url'), '/'),
+ 'theme_url' => $this->getConfig('themes_url') . $this->getTheme(),
'site_title' => $this->getConfig('site_title'),
'meta' => $this->meta,
- 'content' => $this->content,
+ 'content' => new Twig_Markup($this->content, 'UTF-8'),
'pages' => $this->pages,
- 'prev_page' => $this->previousPage,
+ 'previous_page' => $this->previousPage,
'current_page' => $this->currentPage,
'next_page' => $this->nextPage,
'version' => static::VERSION
@@ -2102,6 +2243,29 @@ class Pico
return $this->config['rewrite_url'];
}
+ /**
+ * Returns TRUE if Pico's debug mode is enabled
+ *
+ * @return bool TRUE if Pico's debug mode is enabled, FALSE otherwise
+ */
+ public function isDebugModeEnabled()
+ {
+ $debugModeEnabled = $this->getConfig('debug');
+ if ($debugModeEnabled !== null) {
+ return $debugModeEnabled;
+ }
+
+ if (isset($_SERVER['PICO_DEBUG'])) {
+ $this->config['debug'] = (bool) $_SERVER['PICO_DEBUG'];
+ } elseif (isset($_SERVER['REDIRECT_PICO_DEBUG'])) {
+ $this->config['debug'] = (bool) $_SERVER['REDIRECT_PICO_DEBUG'];
+ } else {
+ $this->config['debug'] = false;
+ }
+
+ return $this->config['debug'];
+ }
+
/**
* Returns the URL to a given page
*
@@ -2115,6 +2279,8 @@ class Pico
* "index", passing TRUE (default) will remove this path component
*
* @return string URL
+ *
+ * @throws InvalidArgumentException thrown when invalid arguments got passed
*/
public function getPageUrl($page, $queryData = null, $dropIndex = true)
{
@@ -2122,7 +2288,7 @@ class Pico
$queryData = http_build_query($queryData, '', '&');
} elseif (($queryData !== null) && !is_string($queryData)) {
throw new InvalidArgumentException(
- 'Argument 2 passed to ' . get_called_class() . '::getPageUrl() must be of the type array or string, '
+ 'Argument 2 passed to ' . __METHOD__ . ' must be of the type array or string, '
. (is_object($queryData) ? get_class($queryData) : gettype($queryData)) . ' given'
);
}
@@ -2179,42 +2345,93 @@ class Pico
return substr($path, $contentDirLength, -$contentExtLength) ?: null;
}
+ /**
+ * Substitutes URL placeholders (e.g. %base_url%)
+ *
+ * This method is registered as the `url` Twig filter and often used to
+ * allow users to specify absolute URLs in meta data utilizing the known
+ * URL placeholders `%base_url%`, `%plugins_url%`, `%themes_url%`,
+ * `%assets_url%` and `%theme_url%`.
+ *
+ * Don't confuse this with the `link` Twig filter, which takes a page ID as
+ * parameter. However, you can indeed use this method to create page URLs,
+ * e.g. `{{ "%base_url%?sub/page"|url }}`.
+ *
+ * @param string $url URL with placeholders
+ *
+ * @return string URL with replaced placeholders
+ */
+ public function substituteUrl($url)
+ {
+ $variables = array(
+ '%base_url%?' => $this->getBaseUrl() . (!$this->isUrlRewritingEnabled() ? '?' : ''),
+ '%base_url%' => rtrim($this->getBaseUrl(), '/'),
+ '%plugins_url%' => rtrim($this->getConfig('plugins_url'), '/'),
+ '%themes_url%' => rtrim($this->getConfig('themes_url'), '/'),
+ '%assets_url%' => rtrim($this->getConfig('assets_url'), '/'),
+ '%theme_url%' => $this->getConfig('themes_url') . $this->getTheme()
+ );
+
+ return str_replace(array_keys($variables), $variables, $url);
+ }
+
/**
* Returns the URL of the themes folder of this Pico instance
*
- * We assume that the themes folder is a arbitrary deep sub folder of the
- * script's base path (i.e. the directory {@path "index.php"} is in resp.
- * the `httpdocs` directory). Usually the script's base path is identical
- * to {@see Pico::$rootDir}, but this may aberrate when Pico got installed
- * as a composer dependency. However, ultimately it allows us to use
- * {@see Pico::getBaseUrl()} as origin of the theme URL. Otherwise Pico
- * falls back to the basename of {@see Pico::$themesDir} (i.e. assuming
- * that `Pico::$themesDir` is `foo/bar/baz`, the base URL of the themes
- * folder will be `baz/`; this ensures BC to Pico < 2.0). Pico's base URL
- * always gets prepended appropriately.
+ * @see Pico::getUrlFromPath()
*
- * @return string the URL of the themes folder
+ * @deprecated 2.1.0
+ *
+ * @return string
*/
public function getBaseThemeUrl()
{
- $themeUrl = $this->getConfig('theme_url');
- if ($themeUrl) {
- return $themeUrl;
- }
+ return $this->getConfig('themes_url');
+ }
- if (isset($_SERVER['SCRIPT_FILENAME']) && ($_SERVER['SCRIPT_FILENAME'] !== 'index.php')) {
+ /**
+ * Returns the URL of a given absolute path within this Pico instance
+ *
+ * We assume that the given path is a arbitrary deep sub folder of the
+ * script's base path (i.e. the directory {@path "index.php"} is in resp.
+ * the `httpdocs` directory). If this isn't the case, we check whether it's
+ * a sub folder of {@see Pico::$rootDir} (what is often identical to the
+ * script's base path). If this isn't the case either, we fall back to
+ * the basename of the given folder. This whole process ultimately allows
+ * us to use {@see Pico::getBaseUrl()} as origin for the URL.
+ *
+ * This method is used to guess Pico's `plugins_url`, `themes_url` and
+ * `assets_url`. However, guessing might fail, requiring a manual config.
+ *
+ * @param string $absolutePath the absolute path to interpret
+ *
+ * @return string the URL of the given folder
+ */
+ public function getUrlFromPath($absolutePath)
+ {
+ $absolutePath = str_replace('\\', '/', $absolutePath);
+
+ $basePath = '';
+ if (isset($_SERVER['SCRIPT_FILENAME']) && strrpos($_SERVER['SCRIPT_FILENAME'], '/')) {
$basePath = dirname($_SERVER['SCRIPT_FILENAME']);
$basePath = !in_array($basePath, array('.', '/', '\\'), true) ? $basePath . '/' : '/';
$basePathLength = strlen($basePath);
- if (substr($this->getThemesDir(), 0, $basePathLength) === $basePath) {
- $this->config['theme_url'] = $this->getBaseUrl() . substr($this->getThemesDir(), $basePathLength);
- return $this->config['theme_url'];
+ if ((substr($absolutePath, 0, $basePathLength) === $basePath) && ($basePath !== '/')) {
+ return $this->getBaseUrl() . substr($absolutePath, $basePathLength);
}
}
- $this->config['theme_url'] = $this->getBaseUrl() . basename($this->getThemesDir()) . '/';
- return $this->config['theme_url'];
+ if ($basePath !== $this->getRootDir()) {
+ $basePath = $this->getRootDir();
+ $basePathLength = strlen($basePath);
+
+ if (substr($absolutePath, 0, $basePathLength) === $basePath) {
+ return $this->getBaseUrl() . substr($absolutePath, $basePathLength);
+ }
+ }
+
+ return $this->getBaseUrl() . basename($absolutePath) . '/';
}
/**
@@ -2367,11 +2584,10 @@ class Pico
public function getFiles($directory, $fileExtension = '', $order = self::SORT_ASC)
{
$directory = rtrim($directory, '/');
+ $fileExtensionLength = strlen($fileExtension);
$result = array();
- // scandir() reads files in alphabetical order
$files = scandir($directory, $order);
- $fileExtensionLength = strlen($fileExtension);
if ($files !== false) {
foreach ($files as $file) {
// exclude hidden files/dirs starting with a .; this also excludes the special dirs . and ..
@@ -2430,24 +2646,109 @@ class Pico
/**
* Makes a relative path absolute to Pico's root dir
*
- * This method also guarantees a trailing slash.
- *
- * @param string $path relative or absolute path
+ * @param string $path relative or absolute path
+ * @param string $basePath treat relative paths relative to the given path;
+ * defaults to Pico::$rootDir
+ * @param bool $endSlash whether to add a trailing slash to the absolute
+ * path or not (defaults to TRUE)
*
* @return string absolute path
*/
- public function getAbsolutePath($path)
+ public function getAbsolutePath($path, $basePath = null, $endSlash = true)
{
+ if ($basePath === null) {
+ $basePath = $this->getRootDir();
+ }
+
if (DIRECTORY_SEPARATOR === '\\') {
if (preg_match('/^(?>[a-zA-Z]:\\\\|\\\\\\\\)/', $path) !== 1) {
- $path = $this->getRootDir() . $path;
+ $path = $basePath . $path;
}
} else {
if ($path[0] !== '/') {
- $path = $this->getRootDir() . $path;
+ $path = $basePath . $path;
}
}
- return rtrim($path, '/\\') . '/';
+
+ return rtrim($path, '/\\') . ($endSlash ? '/' : '');
+ }
+
+ /**
+ * Normalizes a path by taking care of '', '.' and '..' parts
+ *
+ * @param string $path path to normalize
+ * @param bool $allowAbsolutePath whether absolute paths are allowed
+ * @param bool $endSlash whether to add a trailing slash to the
+ * normalized path or not (defaults to TRUE)
+ *
+ * @return string normalized path
+ *
+ * @throws UnexpectedValueException thrown when a absolute path is passed
+ * although absolute paths aren't allowed
+ */
+ public function getNormalizedPath($path, $allowAbsolutePath = false, $endSlash = true)
+ {
+ $absolutePath = '';
+ if (DIRECTORY_SEPARATOR === '\\') {
+ if (preg_match('/^(?>[a-zA-Z]:\\\\|\\\\\\\\)/', $path, $pathMatches) === 1) {
+ $absolutePath = $pathMatches[0];
+ $path = substr($path, strlen($absolutePath));
+ }
+ } else {
+ if ($path[0] === '/') {
+ $absolutePath = '/';
+ $path = substr($path, 1);
+ }
+ }
+
+ if ($absolutePath && !$allowAbsolutePath) {
+ throw new UnexpectedValueException(
+ 'Argument 1 passed to ' . __METHOD__ . ' must be a relative path, absolute path "' . $path . '" given'
+ );
+ }
+
+ $path = str_replace('\\', '/', $path);
+ $pathParts = explode('/', $path);
+
+ $resultParts = array();
+ foreach ($pathParts as $pathPart) {
+ if (($pathPart === '') || ($pathPart === '.')) {
+ continue;
+ } elseif ($pathPart === '..') {
+ array_pop($resultParts);
+ continue;
+ }
+ $resultParts[] = $pathPart;
+ }
+
+ if (!$resultParts) {
+ return $absolutePath ?: '/';
+ }
+
+ return $absolutePath . implode('/', $resultParts) . ($endSlash ? '/' : '');
+ }
+
+ /**
+ * Makes a relative URL absolute to Pico's base URL
+ *
+ * Please note that URLs starting with a slash are considered absolute URLs
+ * even though they don't include a scheme and host.
+ *
+ * @param string $url relative or absolute URL
+ * @param string $baseUrl treat relative URLs relative to the given URL;
+ * defaults to Pico::getBaseUrl()
+ * @param bool $endSlash whether to add a trailing slash to the absolute
+ * URL or not (defaults to TRUE)
+ *
+ * @return string absolute URL
+ */
+ public function getAbsoluteUrl($url, $baseUrl = null, $endSlash = true)
+ {
+ if (($url[0] !== '/') && !preg_match('#^[A-Za-z][A-Za-z0-9+\-.]*://#', $url)) {
+ $url = (($baseUrl !== null) ? $baseUrl : $this->getBaseUrl()) . $url;
+ }
+
+ return rtrim($url, '/') . ($endSlash ? '/' : '');
}
/**
@@ -2467,8 +2768,6 @@ class Pico
*
* @param string $eventName name of the event to trigger
* @param array $params optional parameters to pass
- *
- * @return void
*/
public function triggerEvent($eventName, array $params = array())
{
diff --git a/lib/PicoPluginInterface.php b/lib/PicoPluginInterface.php
index 13a30dd..85f9dfb 100644
--- a/lib/PicoPluginInterface.php
+++ b/lib/PicoPluginInterface.php
@@ -26,24 +26,15 @@
* @author Daniel Rudolf
* @link http://picocms.org
* @license http://opensource.org/licenses/MIT The MIT License
- * @version 2.0
+ * @version 2.1
*/
interface PicoPluginInterface
{
- /**
- * Constructs a new instance of a Pico plugin
- *
- * @param Pico $pico current instance of Pico
- */
- public function __construct(Pico $pico);
-
/**
* Handles a event that was triggered by Pico
*
* @param string $eventName name of the triggered event
* @param array $params passed parameters
- *
- * @return void
*/
public function handleEvent($eventName, array $params);
@@ -63,8 +54,6 @@ interface PicoPluginInterface
* @param bool $auto enable or disable to fulfill a dependency. This
* parameter is optional and defaults to FALSE.
*
- * @return void
- *
* @throws RuntimeException thrown when a dependency fails
*/
public function setEnabled($enabled, $recursive = true, $auto = false);
@@ -106,11 +95,11 @@ interface PicoPluginInterface
public function getDependants();
/**
- * Returns the plugins instance of Pico
+ * Returns the plugin's instance of Pico
*
* @see Pico
*
- * @return Pico the plugins instance of Pico
+ * @return Pico the plugin's instance of Pico
*/
public function getPico();
}
diff --git a/lib/PicoTwigExtension.php b/lib/PicoTwigExtension.php
index a774184..0988110 100644
--- a/lib/PicoTwigExtension.php
+++ b/lib/PicoTwigExtension.php
@@ -16,7 +16,7 @@
* @author Daniel Rudolf
* @link http://picocms.org
* @license http://opensource.org/licenses/MIT The MIT License
- * @version 2.0
+ * @version 2.1
*/
class PicoTwigExtension extends Twig_Extension
{
@@ -72,10 +72,15 @@ class PicoTwigExtension extends Twig_Extension
public function getFilters()
{
return array(
- 'markdown' => new Twig_SimpleFilter('markdown', array($this, 'markdownFilter')),
+ 'markdown' => new Twig_SimpleFilter(
+ 'markdown',
+ array($this, 'markdownFilter'),
+ array('is_safe' => array('html'))
+ ),
'map' => new Twig_SimpleFilter('map', array($this, 'mapFilter')),
'sort_by' => new Twig_SimpleFilter('sort_by', array($this, 'sortByFilter')),
- 'link' => new Twig_SimpleFilter('link', array($this->pico, 'getPageUrl'))
+ 'link' => new Twig_SimpleFilter('link', array($this->pico, 'getPageUrl')),
+ 'url' => new Twig_SimpleFilter('url', array($this->pico, 'substituteUrl'))
);
}
@@ -90,7 +95,8 @@ class PicoTwigExtension extends Twig_Extension
{
return array(
'url_param' => new Twig_SimpleFunction('url_param', array($this, 'urlParamFunction')),
- 'form_param' => new Twig_SimpleFunction('form_param', array($this, 'formParamFunction'))
+ 'form_param' => new Twig_SimpleFunction('form_param', array($this, 'formParamFunction')),
+ 'pages' => new Twig_SimpleFunction('pages', array($this, 'pagesFunction'))
);
}
@@ -105,15 +111,16 @@ class PicoTwigExtension extends Twig_Extension
* @see Pico::substituteFileContent()
* @see Pico::parseFileContent()
*
- * @param string $markdown markdown to parse
- * @param array $meta meta data to use for %meta.*% replacement
+ * @param string $markdown markdown to parse
+ * @param array $meta meta data to use for %meta.*% replacement
+ * @param bool $singleLine whether to parse just a single line of markup
*
* @return string parsed HTML
*/
- public function markdownFilter($markdown, array $meta = array())
+ public function markdownFilter($markdown, array $meta = array(), $singleLine = false)
{
$markdown = $this->getPico()->substituteFileContent($markdown, $meta);
- return $this->getPico()->parseFileContent($markdown);
+ return $this->getPico()->parseFileContent($markdown, $singleLine);
}
/**
@@ -128,6 +135,8 @@ class PicoTwigExtension extends Twig_Extension
* $item['foo']['bar'] values)
*
* @return array mapped values
+ *
+ * @throws Twig_Error_Runtime
*/
public function mapFilter($var, $mapKeyPath)
{
@@ -168,6 +177,8 @@ class PicoTwigExtension extends Twig_Extension
* these items
*
* @return array sorted array
+ *
+ * @throws Twig_Error_Runtime
*/
public function sortByFilter($var, $sortKeyPath, $fallback = 'bottom')
{
@@ -339,4 +350,138 @@ class PicoTwigExtension extends Twig_Extension
return $this->pico->getFormParameter($name, $filter, $options, $flags);
}
+
+ /**
+ * Returns all pages within a particular branch of Pico's page tree
+ *
+ * This function should be used most of the time when dealing with Pico's
+ * pages array, as it allows one to easily traverse Pico's pages tree
+ * ({@see Pico::getPageTree()}) to retrieve a subset of Pico's pages array
+ * in a very convenient and performant way.
+ *
+ * The function's default parameters are `$start = ""`, `$depth = 0`,
+ * `$depthOffset = 0` and `$offset = 1`. A positive `$offset` is equivalent
+ * to `$depth = $depth + $offset`, `$depthOffset = $depthOffset + $offset`
+ * and `$offset = 0`.
+ *
+ * Consequently the default `$start = ""`, `$depth = 0`, `$depthOffset = 0`
+ * and `$offset = 1` is equivalent to `$depth = 1`, `$depthOffset = 1` and
+ * `$offset = 0`. `$start = ""` instruct the function to start from the
+ * root node (i.e. the node of Pico's main index page at `index.md`).
+ * `$depth` tells the function what pages to return. In this example,
+ * `$depth = 1` matches the start node (i.e. the zeroth generation) and all
+ * its descendant pages until the first generation (i.e. the start node's
+ * children). `$depthOffset` instructs the function to exclude some of the
+ * older generations. `$depthOffset = 1` specifically tells the function
+ * to exclude the zeroth generation, so that the function returns all of
+ * Pico's main index page's direct child pages (like `sub/index.md` and
+ * `page.md`, but not `sub/page.md`) only.
+ *
+ * Passing `$depthOffset = -1` only is the same as passing `$start = ""`,
+ * `$depth = 1`, `$depthOffset = 0` and `$offset = 0`. The only difference
+ * is that `$depthOffset` won't exclude the zeroth generation, so that the
+ * function returns Pico's main index page as well as all of its direct
+ * child pages.
+ *
+ * Passing `$depth = 0`, `$depthOffset = -2` and `$offset = 2` is the same
+ * as passing `$depth = 2`, `$depthOffset = 0` and `$offset = 0`. Both will
+ * return the zeroth, first and second generation of pages. For Pico's main
+ * index page this would be `index.md` (0th gen), `sub/index.md` (1st gen),
+ * `sub/page.md` (2nd gen) and `page.md` (1st gen). If you want to return
+ * 2nd gen pages only, pass `$offset = 2` only (with implicit `$depth = 0`
+ * and `$depthOffset = 0` it's the same as `$depth = 2`, `$depthOffset = 2`
+ * and `$offset = 0`).
+ *
+ * Instead of an integer you can also pass `$depth = null`. This is the
+ * same as passing an infinitely large number as `$depth`, so that this
+ * function simply returns all descendant pages. Consequently passing
+ * `$start = ""`, `$depth = null`, `$depthOffset = 0` and `$offset = 0`
+ * returns Pico's full pages array.
+ *
+ * If `$depth` is negative after taking `$offset` into consideration, the
+ * function will throw a {@see Twig_Error_Runtime} exception, since this
+ * would simply make no sense and is likely an error. Passing a negative
+ * `$depthOffset` is equivalent to passing `$depthOffset = 0`.
+ *
+ * But what about a negative `$offset`? Passing `$offset = -1` instructs
+ * the function not to start from the given `$start` node, but its parent
+ * node. Consequently `$offset = -2` instructs the function to use the
+ * `$start` node's grandparent node. Obviously this won't make any sense
+ * for Pico's root node, but just image `$start = "sub/index"`. Passing
+ * this together with `$offset = -1` is equivalent to `$start = ""` and
+ * `$offset = 0`.
+ *
+ * @param string $start name of the node to start from
+ * @param int|null $depth return pages until the given maximum depth;
+ * pass NULL to return all descendant pages; defaults to 0
+ * @param int $depthOffset start returning pages from the given
+ * minimum depth; defaults to 0
+ * @param int $offset ascend (positive) or descend (negative) the
+ * given number of branches before returning pages; defaults to 1
+ *
+ * @return array[] the data of the matched pages
+ *
+ * @throws Twig_Error_Runtime
+ */
+ public function pagesFunction($start = '', $depth = 0, $depthOffset = 0, $offset = 1)
+ {
+ $start = (string) $start;
+ if (basename($start) === 'index') {
+ $start = dirname($start);
+ }
+
+ for (; $offset < 0; $offset++) {
+ if (in_array($start, array('', '.', '/'), true)) {
+ $offset = 0;
+ break;
+ }
+
+ $start = dirname($start);
+ }
+
+ $depth = ($depth !== null) ? $depth + $offset : null;
+ $depthOffset = $depthOffset + $offset;
+
+ if (($depth !== null) && ($depth < 0)) {
+ throw new Twig_Error_Runtime('The pages function doesn\'t support negative depths');
+ }
+
+ $pageTree = $this->getPico()->getPageTree();
+ if (in_array($start, array('', '.', '/'), true)) {
+ if (($depth === null) && ($depthOffset <= 0)) {
+ return $this->getPico()->getPages();
+ }
+
+ $startNode = isset($pageTree['']['/']) ? $pageTree['']['/'] : null;
+ } else {
+ $branch = dirname($start);
+ $branch = ($branch !== '.') ? $branch : '/';
+ $node = (($branch !== '/') ? $branch . '/' : '') . basename($start);
+ $startNode = isset($pageTree[$branch][$node]) ? $pageTree[$branch][$node] : null;
+ }
+
+ if (!$startNode) {
+ return array();
+ }
+
+ $getPagesClosure = function ($nodes, $depth, $depthOffset) use (&$getPagesClosure) {
+ $pages = array();
+ foreach ($nodes as $node) {
+ if (isset($node['page']) && ($depthOffset <= 0)) {
+ $pages[$node['page']['id']] = &$node['page'];
+ }
+ if (isset($node['children']) && ($depth > 0)) {
+ $pages += $getPagesClosure($node['children'], $depth - 1, $depthOffset - 1);
+ }
+ }
+
+ return $pages;
+ };
+
+ return $getPagesClosure(
+ array($startNode),
+ ($depth !== null) ? $depth : INF,
+ $depthOffset
+ );
+ }
}
diff --git a/plugins/DummyPlugin.php b/plugins/DummyPlugin.php
index effc980..415103b 100644
--- a/plugins/DummyPlugin.php
+++ b/plugins/DummyPlugin.php
@@ -19,7 +19,7 @@
* @author Daniel Rudolf
* @link http://picocms.org
* @license http://opensource.org/licenses/MIT The MIT License
- * @version 2.0
+ * @version 2.1
*/
class DummyPlugin extends AbstractPicoPlugin
{
@@ -36,8 +36,8 @@ class DummyPlugin extends AbstractPicoPlugin
* Usually you should remove this class property (or set it to NULL) to
* leave the decision whether this plugin should be enabled or disabled by
* default up to Pico. If all the plugin's dependenies are fulfilled (see
- * {@see self::$dependsOn}), Pico enables the plugin by default. Otherwise
- * the plugin is silently disabled.
+ * {@see DummyPlugin::$dependsOn}), Pico enables the plugin by default.
+ * Otherwise the plugin is silently disabled.
*
* If this plugin should never be disabled *silently* (e.g. when dealing
* with security-relevant stuff like access control, or similar), set this
@@ -79,8 +79,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getPlugins()
*
* @param object[] $plugins loaded plugin instances
- *
- * @return void
*/
public function onPluginsLoaded(array $plugins)
{
@@ -95,8 +93,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getPlugins()
*
* @param object $plugin loaded plugin instance
- *
- * @return void
*/
public function onPluginManuallyLoaded($plugin)
{
@@ -108,26 +104,50 @@ class DummyPlugin extends AbstractPicoPlugin
*
* @see Pico::getConfig()
* @see Pico::getBaseUrl()
- * @see Pico::getBaseThemeUrl()
* @see Pico::isUrlRewritingEnabled()
*
* @param array &$config array of config variables
- *
- * @return void
*/
public function onConfigLoaded(array &$config)
{
// your code
}
+ /**
+ * Triggered before Pico loads its theme
+ *
+ * @see Pico::loadTheme()
+ * @see DummyPlugin::onThemeLoaded()
+ *
+ * @param string &$theme name of current theme
+ */
+ public function onThemeLoading(&$theme)
+ {
+ // your code
+ }
+
+ /**
+ * Triggered after Pico loaded its theme
+ *
+ * @see DummyPlugin::onThemeLoading()
+ * @see Pico::getTheme()
+ * @see Pico::getThemeApiVersion()
+ *
+ * @param string $theme name of current theme
+ * @param int $themeApiVersion API version of the theme
+ * @param array &$themeConfig config array of the theme
+ */
+ public function onThemeLoaded($theme, $themeApiVersion, array &$themeConfig)
+ {
+ // your code
+ }
+
/**
* Triggered after Pico has evaluated the request URL
*
* @see Pico::getRequestUrl()
*
* @param string &$url part of the URL describing the requested contents
- *
- * @return void
*/
public function onRequestUrl(&$url)
{
@@ -141,8 +161,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getRequestFile()
*
* @param string &$file absolute path to the content file to serve
- *
- * @return void
*/
public function onRequestFile(&$file)
{
@@ -154,8 +172,6 @@ class DummyPlugin extends AbstractPicoPlugin
*
* @see Pico::loadFileContent()
* @see DummyPlugin::onContentLoaded()
- *
- * @return void
*/
public function onContentLoading()
{
@@ -167,8 +183,6 @@ class DummyPlugin extends AbstractPicoPlugin
*
* @see Pico::load404Content()
* @see DummyPlugin::on404ContentLoaded()
- *
- * @return void
*/
public function on404ContentLoading()
{
@@ -183,8 +197,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::is404Content()
*
* @param string &$rawContent raw file contents
- *
- * @return void
*/
public function on404ContentLoaded(&$rawContent)
{
@@ -203,8 +215,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::is404Content()
*
* @param string &$rawContent raw file contents
- *
- * @return void
*/
public function onContentLoaded(&$rawContent)
{
@@ -216,8 +226,6 @@ class DummyPlugin extends AbstractPicoPlugin
*
* @see Pico::parseFileMeta()
* @see DummyPlugin::onMetaParsed()
- *
- * @return void
*/
public function onMetaParsing()
{
@@ -231,8 +239,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getFileMeta()
*
* @param string[] &$meta parsed meta data
- *
- * @return void
*/
public function onMetaParsed(array &$meta)
{
@@ -246,8 +252,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::substituteFileContent()
* @see DummyPlugin::onContentPrepared()
* @see DummyPlugin::onContentParsed()
- *
- * @return void
*/
public function onContentParsing()
{
@@ -262,8 +266,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see DummyPlugin::onContentParsed()
*
* @param string &$markdown Markdown contents of the requested page
- *
- * @return void
*/
public function onContentPrepared(&$markdown)
{
@@ -278,8 +280,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getFileContent()
*
* @param string &$content parsed contents (HTML) of the requested page
- *
- * @return void
*/
public function onContentParsed(&$content)
{
@@ -291,8 +291,6 @@ class DummyPlugin extends AbstractPicoPlugin
*
* @see DummyPlugin::onPagesDiscovered()
* @see DummyPlugin::onPagesLoaded()
- *
- * @return void
*/
public function onPagesLoading()
{
@@ -314,8 +312,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @param string $id relative path to the content file
* @param bool|null $skipPage set this to TRUE to remove this page from the
* pages array, otherwise leave it unchanged
- *
- * @return void
*/
public function onSinglePageLoading($id, &$skipPage)
{
@@ -334,8 +330,6 @@ class DummyPlugin extends AbstractPicoPlugin
*
* @param string $id relative path to the content file
* @param string &$rawContent raw file contents
- *
- * @return void
*/
public function onSinglePageContent($id, &$rawContent)
{
@@ -352,8 +346,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see DummyPlugin::onSinglePageContent()
*
* @param array &$pageData data of the loaded page
- *
- * @return void
*/
public function onSinglePageLoaded(array &$pageData)
{
@@ -372,8 +364,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see DummyPlugin::onPagesLoaded()
*
* @param array[] &$pages list of all known pages
- *
- * @return void
*/
public function onPagesDiscovered(array &$pages)
{
@@ -392,8 +382,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getPages()
*
* @param array[] &$pages sorted list of all known pages
- *
- * @return void
*/
public function onPagesLoaded(array &$pages)
{
@@ -415,8 +403,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @param array|null &$currentPage data of the page being served
* @param array|null &$previousPage data of the previous page
* @param array|null &$nextPage data of the next page
- *
- * @return void
*/
public function onCurrentPageDiscovered(
array &$currentPage = null,
@@ -435,8 +421,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getPageTree()
*
* @param array &$pageTree page tree
- *
- * @return void
*/
public function onPageTreeBuilt(array &$pageTree)
{
@@ -450,8 +434,6 @@ class DummyPlugin extends AbstractPicoPlugin
*
* @param string &$templateName file name of the template
* @param array &$twigVariables template variables
- *
- * @return void
*/
public function onPageRendering(&$templateName, array &$twigVariables)
{
@@ -464,8 +446,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see DummyPlugin::onPageRendering()
*
* @param string &$output contents which will be sent to the user
- *
- * @return void
*/
public function onPageRendered(&$output)
{
@@ -480,8 +460,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @param string[] &$headers list of known meta header fields; the array
* key specifies the YAML key to search for, the array value is later
* used to access the found value
- *
- * @return void
*/
public function onMetaHeaders(array &$headers)
{
@@ -494,8 +472,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getYamlParser()
*
* @param \Symfony\Component\Yaml\Parser &$yamlParser YAML parser instance
- *
- * @return void
*/
public function onYamlParserRegistered(\Symfony\Component\Yaml\Parser &$yamlParser)
{
@@ -508,8 +484,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getParsedown()
*
* @param Parsedown &$parsedown Parsedown instance
- *
- * @return void
*/
public function onParsedownRegistered(Parsedown &$parsedown)
{
@@ -522,8 +496,6 @@ class DummyPlugin extends AbstractPicoPlugin
* @see Pico::getTwig()
*
* @param Twig_Environment &$twig Twig instance
- *
- * @return void
*/
public function onTwigRegistered(Twig_Environment &$twig)
{