commit b5baa037bf6cd072c9c4cd680f081d3d8ac90ee6 Author: Armin Ronacher Date: Sat Dec 19 14:52:17 2015 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4cdfd605 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +*.pyc +*.egg-info +.cache +packages/*/build +packages/*/dist +webpack/node_modules diff --git a/README.md b/README.md new file mode 100644 index 00000000..62c9a9c8 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Lektor Website + +This is the work in progress website for Lektor. This also includes +documentation. + +To run: + +``` +lektor dev +``` + +If you also want to update the webpack files, you need `npm` installed +and then run it like this: + +``` +LEKTOR_WEBPACK=1 lektor dev +``` diff --git a/Website.lektorproject b/Website.lektorproject new file mode 100644 index 00000000..79bb34d0 --- /dev/null +++ b/Website.lektorproject @@ -0,0 +1,13 @@ +[project] +name = Lektor +url = https://www.getlektor.com/ + +[servers.production] +enabled = yes +name = Production +target = rsync://flow.srv.pocoo.org/srv/websites/getlektor.com/static +default = yes + +[packages] +lektor-webpack-support = 0.1 +lektor-disqus-comments = 0.1 diff --git a/assets/install.bat b/assets/install.bat new file mode 100644 index 00000000..bd7c0f81 --- /dev/null +++ b/assets/install.bat @@ -0,0 +1,115 @@ +@echo off & python --version 2> NUL & IF ERRORLEVEL 1 ( ECHO To use this script you need to have Python installed & goto :eof ) ELSE ( python -x "%~f0" %* & goto :eof ) + +import os +import sys +import json +import urllib +import tempfile +import tarfile +from subprocess import Popen +from _winreg import OpenKey, CloseKey, QueryValueEx, SetValueEx, \ + HKEY_CURRENT_USER, KEY_ALL_ACCESS, REG_EXPAND_SZ +import ctypes +from ctypes.wintypes import HWND, UINT, WPARAM, LPARAM, LPVOID + + +VENV_URL = "https://pypi.python.org/pypi/virtualenv/json" +APPDATA = os.environ['LocalAppData'] +APP = 'lektor-cli' +LIB = 'lib' +ROOT_KEY = HKEY_CURRENT_USER +SUB_KEY = 'Environment' +LRESULT = LPARAM +HWND_BROADCAST = 0xFFFF +WM_SETTINGCHANGE = 0x1A + +def find_location(): + install_dir = os.path.join(APPDATA, APP) + if os.path.exists(install_dir) and os.path.isdir(install_dir): + return None, None + return install_dir, os.path.join(install_dir, LIB) + +def fail(message): + print 'Error: %s' % message + sys.exit(1) + +def add_to_path(location): + reg_key = OpenKey(ROOT_KEY, SUB_KEY, 0, KEY_ALL_ACCESS) + + try: + path_value, _ = QueryValueEx(reg_key, 'Path') + except WindowsError: + path_value = '' + + paths = path_value.split(';') + if location not in paths: + paths.append(location) + path_value = ';'.join(paths) + SetValueEx(reg_key, 'Path', 0, REG_EXPAND_SZ, path_value) + + SendMessage = ctypes.windll.user32.SendMessageW + SendMessage.argtypes = HWND, UINT, WPARAM, LPVOID + SendMessage.restype = LRESULT + SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, u'Environment') + +def install(virtualenv_url, virtualenv_filename, install_dir, lib_dir): + t = tempfile.mkdtemp() + with open(os.path.join(t, 'virtualenv.tar.gz'), 'wb') as f: + download = urllib.urlopen(virtualenv_url) + f.write(download.read()) + download.close() + with tarfile.open(os.path.join(t, 'virtualenv.tar.gz'), 'r:gz') as tar: + tar.extractall(path=t) + + os.makedirs(install_dir) + os.makedirs(lib_dir) + + Popen(['python', 'virtualenv.py', lib_dir], + cwd=os.path.join(t, virtualenv_filename)).wait() + scripts = os.path.join(lib_dir, 'Scripts') + #just using pip.exe and cwd will still install globally + Popen([os.path.join(scripts, 'pip.exe'), + 'install', '--upgrade', 'git+https://github.com/lektor/lektor'], + cwd=scripts).wait() + + with open(os.path.join(install_dir, 'lektor.cmd'), 'w') as link_file: + link_file.write('@echo off\n') + link_file.write('\"' + os.path.join(scripts, 'lektor.exe') + '\"' + ' %*') + + add_to_path(install_dir) + + +def main(): + print 'Welcome to Lektor' + print + print 'This script will install Lektor on your computer.' + print + + install_dir, lib_dir = find_location() + if install_dir == None: + fail('Lektor seems to be installed already.') + + print ' Installing at: %s' % install_dir + while 1: + input = raw_input('Continue? [Yn] ').lower().strip() + if input in ('', 'y'): + break + elif input == 'n': + print 'Aborted!' + sys.exit() + + for url in json.load(urllib.urlopen(VENV_URL))['urls']: + if url['python_version'] == 'source': + virtualenv_url = url['url'] + #stripping '.tar.gz' + virtualenv_filename = url['filename'][:-7] + break + else: + fail('Could not find virtualenv') + + install(virtualenv_url, virtualenv_filename, install_dir, lib_dir) + + print + print 'All done!' + +main() diff --git a/assets/install.sh b/assets/install.sh new file mode 100644 index 00000000..892ddf57 --- /dev/null +++ b/assets/install.sh @@ -0,0 +1,123 @@ +#!/bin/sh +# This script helps you install Lektor on your computer. Right now it +# only supports Linux and OS X and only on OS X will it install the +# desktop version. +# +# For more information see https://www.getlektor.com/ + +# Wrap everything in a function so that we do not accidentally execute +# something we should not in case a truncated version of the script +# is executed. +I() { + set -u + + if ! hash python 2> /dev/null; then + echo "Error: To use this script you need to have Python installed" + exit 1 + fi + + python - <<'EOF' +if 1: + + import os + import sys + import json + import urllib + import tempfile + from subprocess import Popen + + sys.stdin = open('/dev/tty', 'r') + + VENV_URL = "https://pypi.python.org/pypi/virtualenv/json" + KNOWN_BINS = ['/usr/local/bin', '/opt/local/bin', + os.path.join(os.environ['HOME'], '.bin'), + os.path.join(os.environ['HOME'], '.local', 'bin')] + + def find_user_paths(): + rv = [] + for item in os.environ['PATH'].split(':'): + if os.access(item, os.W_OK) \ + and item not in rv \ + and '/sbin' not in item: + rv.append(item) + return rv + + def bin_sort_key(path): + try: + return KNOWN_BINS.index(path) + except ValueError: + return float('inf') + + def find_locations(paths): + paths.sort(key=bin_sort_key) + for path in paths: + if path.startswith(os.environ['HOME']): + return path, os.path.join(os.environ['HOME'], + '.local', 'lib', 'lektor') + elif path.endswith('/bin'): + return path, os.path.join( + os.path.dirname(path), 'lib', 'lektor') + None, None + + def fail(message): + print 'Error: %s' % message + sys.exit(1) + + def install(virtualenv_url, lib_dir, bin_dir): + t = tempfile.mkdtemp() + Popen('curl -sf "%s" | tar -xzf - --strip-components=1' % + virtualenv_url, shell=True, cwd=t).wait() + try: + os.makedirs(lib_dir) + except OSError: + pass + Popen(['./virtualenv.py', lib_dir], cwd=t).wait() + Popen([os.path.join(lib_dir, 'bin', 'pip'), + 'install', '--upgrade', 'Lektor']).wait() + os.symlink(os.path.join(lib_dir, 'bin', 'lektor'), + os.path.join(bin_dir, 'lektor')) + + def main(): + print 'Welcome to Lektor' + print + print 'This script will install Lektor on your computer.' + print + + paths = find_user_paths() + if not paths: + fail('None of the items in $PATH are writable. Run with ' + 'sudo or add a $PATH item that you have access to.') + + bin_dir, lib_dir = find_locations(paths) + if bin_dir is None or lib_dir is None: + fail('Could not determine installation location for Lektor.') + + print ' bin: %s' % bin_dir + print ' app: %s' % lib_dir + print + + while 1: + input = raw_input('Continue? [Yn] ').lower().strip() + if input in ('', 'y'): + break + elif input == 'n': + print 'Aborted!' + sys.exit() + + for url in json.load(urllib.urlopen(VENV_URL))['urls']: + if url['python_version'] == 'source': + virtualenv = url['url'] + break + else: + fail('Could not find virtualenv') + + install(virtualenv, lib_dir, bin_dir) + + print + print 'All done!' + + main() +EOF +} + +I diff --git a/assets/static/20add4b9a80177b140e9953157584556.png b/assets/static/20add4b9a80177b140e9953157584556.png new file mode 100644 index 00000000..6ac86c78 Binary files /dev/null and b/assets/static/20add4b9a80177b140e9953157584556.png differ diff --git a/assets/static/304a42c138b127029843edaff3d4ace2.png b/assets/static/304a42c138b127029843edaff3d4ace2.png new file mode 100644 index 00000000..3361adfa Binary files /dev/null and b/assets/static/304a42c138b127029843edaff3d4ace2.png differ diff --git a/assets/static/32400f4e08932a94d8bfd2422702c446.eot b/assets/static/32400f4e08932a94d8bfd2422702c446.eot new file mode 100644 index 00000000..9b6afaed Binary files /dev/null and b/assets/static/32400f4e08932a94d8bfd2422702c446.eot differ diff --git a/assets/static/448c34a56d699c29117adc64c43affeb.woff2 b/assets/static/448c34a56d699c29117adc64c43affeb.woff2 new file mode 100644 index 00000000..64539b54 Binary files /dev/null and b/assets/static/448c34a56d699c29117adc64c43affeb.woff2 differ diff --git a/assets/static/4d5d06a3788530cf91f0f11cc8b59e0a.png b/assets/static/4d5d06a3788530cf91f0f11cc8b59e0a.png new file mode 100644 index 00000000..2bda3eaa Binary files /dev/null and b/assets/static/4d5d06a3788530cf91f0f11cc8b59e0a.png differ diff --git a/assets/static/89889688147bd7575d6327160d64e760.svg b/assets/static/89889688147bd7575d6327160d64e760.svg new file mode 100644 index 00000000..94fb5490 --- /dev/null +++ b/assets/static/89889688147bd7575d6327160d64e760.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/static/8b306409d3e91ce2e2816d72d96812c3.png b/assets/static/8b306409d3e91ce2e2816d72d96812c3.png new file mode 100644 index 00000000..05825ad0 Binary files /dev/null and b/assets/static/8b306409d3e91ce2e2816d72d96812c3.png differ diff --git a/assets/static/a35720c2fed2c7f043bc7e4ffb45e073.woff b/assets/static/a35720c2fed2c7f043bc7e4ffb45e073.woff new file mode 100644 index 00000000..dc35ce3c Binary files /dev/null and b/assets/static/a35720c2fed2c7f043bc7e4ffb45e073.woff differ diff --git a/assets/static/a3de2170e4e9df77161ea5d3f31b2668.ttf b/assets/static/a3de2170e4e9df77161ea5d3f31b2668.ttf new file mode 100644 index 00000000..26dea795 Binary files /dev/null and b/assets/static/a3de2170e4e9df77161ea5d3f31b2668.ttf differ diff --git a/assets/static/app.js b/assets/static/app.js new file mode 100644 index 00000000..ac7c8c49 --- /dev/null +++ b/assets/static/app.js @@ -0,0 +1,28 @@ +!function(t){function e(i){if(n[i])return n[i].exports;var o=n[i]={exports:{},id:i,loaded:!1};return t[i].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){(function(t){"use strict";function e(t){if(document.body.createTextRange){var e=document.body.createTextRange();e.moveToElementText(t),e.select()}else if(window.getSelection){var n=window.getSelection(),e=document.createRange();e.selectNodeContents(t),n.removeAllRanges(),n.addRange(e)}}function i(){var e=t(".download-btn");e.length<=0||(e.hide(),t.ajax({method:"GET",url:"https://api.github.com/repos/lektor/lektor/releases",crossDomain:!0}).then(function(t){r(e.toArray(),t)},function(){e.show()}))}function o(t){var e=null,n=null;if(navigator.platform.match(/^mac/i)&&(e=/\.dmg$/,n="For OSX 10.9 and later."),null!=e)for(var i=0;i').appendTo(e),o=t("a",e);s?(o.attr("href",s.url),n.append(t("").text(s.note+" ")),n.append(t("Other platforms").attr("href",r))):o.attr("href",r),o.append(t('').text(i)),t(e).fadeIn("slow")})}function s(){var n=t(".install-row pre");n.length>0&&n.on("dblclick",function(){e(this)})}n(2),t(function(){i(),s()})}).call(e,n(1))},function(t,e,n){var i,o;/*! + * jQuery JavaScript Library v2.1.4 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-04-28T16:01Z + */ +!function(e,n){"object"==typeof t&&"object"==typeof t.exports?t.exports=e.document?n(e,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return n(t)}:n(e)}("undefined"!=typeof window?window:this,function(n,r){function s(t){var e="length"in t&&t.length,n=it.type(t);return"function"===n||it.isWindow(t)?!1:1===t.nodeType&&e?!0:"array"===n||0===e||"number"==typeof e&&e>0&&e-1 in t}function a(t,e,n){if(it.isFunction(e))return it.grep(t,function(t,i){return!!e.call(t,i,t)!==n});if(e.nodeType)return it.grep(t,function(t){return t===e!==n});if("string"==typeof e){if(pt.test(e))return it.filter(e,t,n);e=it.filter(e,t)}return it.grep(t,function(t){return K.call(e,t)>=0!==n})}function l(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}function c(t){var e=yt[t]={};return it.each(t.match(vt)||[],function(t,n){e[n]=!0}),e}function u(){et.removeEventListener("DOMContentLoaded",u,!1),n.removeEventListener("load",u,!1),it.ready()}function p(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=it.expando+p.uid++}function d(t,e,n){var i;if(void 0===n&&1===t.nodeType)if(i="data-"+e.replace(Et,"-$1").toLowerCase(),n=t.getAttribute(i),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:Ct.test(n)?it.parseJSON(n):n}catch(o){}Tt.set(t,e,n)}else n=void 0;return n}function f(){return!0}function h(){return!1}function g(){try{return et.activeElement}catch(t){}}function m(t,e){return it.nodeName(t,"table")&&it.nodeName(11!==e.nodeType?e:e.firstChild,"tr")?t.getElementsByTagName("tbody")[0]||t.appendChild(t.ownerDocument.createElement("tbody")):t}function v(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function y(t){var e=Wt.exec(t.type);return e?t.type=e[1]:t.removeAttribute("type"),t}function b(t,e){for(var n=0,i=t.length;i>n;n++)wt.set(t[n],"globalEval",!e||wt.get(e[n],"globalEval"))}function x(t,e){var n,i,o,r,s,a,l,c;if(1===e.nodeType){if(wt.hasData(t)&&(r=wt.access(t),s=wt.set(e,r),c=r.events)){delete s.handle,s.events={};for(o in c)for(n=0,i=c[o].length;i>n;n++)it.event.add(e,o,c[o][n])}Tt.hasData(t)&&(a=Tt.access(t),l=it.extend({},a),Tt.set(e,l))}}function w(t,e){var n=t.getElementsByTagName?t.getElementsByTagName(e||"*"):t.querySelectorAll?t.querySelectorAll(e||"*"):[];return void 0===e||e&&it.nodeName(t,e)?it.merge([t],n):n}function T(t,e){var n=e.nodeName.toLowerCase();"input"===n&&Nt.test(t.type)?e.checked=t.checked:("input"===n||"textarea"===n)&&(e.defaultValue=t.defaultValue)}function C(t,e){var i,o=it(e.createElement(t)).appendTo(e.body),r=n.getDefaultComputedStyle&&(i=n.getDefaultComputedStyle(o[0]))?i.display:it.css(o[0],"display");return o.detach(),r}function E(t){var e=et,n=Ut[t];return n||(n=C(t,e),"none"!==n&&n||(_t=(_t||it(" +{% endmacro %} +``` diff --git a/content/docs/api/databags/get-bag/contents.lr b/content/docs/api/databags/get-bag/contents.lr new file mode 100644 index 00000000..89c8e410 --- /dev/null +++ b/content/docs/api/databags/get-bag/contents.lr @@ -0,0 +1,13 @@ +title: get_bag +--- +type: method +--- +signature: name +--- +summary: Looks up an entire databag by its name. +--- +body: + +This method is a special case of [lookup :ref](../lookup/) which just +looks up an entire data bag and returns it as dictionary. For most +intents and purposes `lookup` is what you want. diff --git a/content/docs/api/databags/lookup/contents.lr b/content/docs/api/databags/lookup/contents.lr new file mode 100644 index 00000000..74aaa147 --- /dev/null +++ b/content/docs/api/databags/lookup/contents.lr @@ -0,0 +1,23 @@ +title: lookup +--- +type: method +--- +signature: key +--- +summary: Looks up data from databags. +--- +body: + +This is the most common way to look up values from databags from regular +Python code. Within templates you can also use the [bag +:ref](../../templates/globals/bag/) function which is easier to call. + +The key is in dotted notation. For more information about this refer +to the main [databags :ref](../) documentation. + +## Example + +```python +def translate(pad, alt, key): + return pad.databags.get('i18n.%s.%s' % (alt, key), key) +``` diff --git a/content/docs/api/db/contents.lr b/content/docs/api/db/contents.lr new file mode 100644 index 00000000..c01bb095 --- /dev/null +++ b/content/docs/api/db/contents.lr @@ -0,0 +1,10 @@ +title: Database +--- +summary: Detailed information about Lektor's flat file tree database system. +--- +body: + +While Lektor does not have a real traditional SQL database, it has a tree +based data layer which is internally called the database. It's a very +powerful tool for building websites and is accessible from plugin API, +the `shell` command as well as from within the templates. diff --git a/content/docs/api/db/expression/and/contents.lr b/content/docs/api/db/expression/and/contents.lr new file mode 100644 index 00000000..7c7a8144 --- /dev/null +++ b/content/docs/api/db/expression/and/contents.lr @@ -0,0 +1,34 @@ +title: and +--- +summary: True if both left and right side are true. +--- +type: method +--- +signature: other +--- +body: + +This evaluates to true if both the expression on the left as well as the +expression on the right are true. This is one of the few operators that +differs between Python and templates. In templates you have to use the +`and` method whereas in Python have to use the `&` operator. + +## Template Example + +```html+jinja +

3 Star or Higher

+ +``` + +## Python Example + +```python +def get_hotels(page): + return page.children.filter( + (F.type == 'hotel') & (F.stars >= 3)) +``` diff --git a/content/docs/api/db/expression/contains/contents.lr b/content/docs/api/db/expression/contains/contents.lr new file mode 100644 index 00000000..32ea681d --- /dev/null +++ b/content/docs/api/db/expression/contains/contents.lr @@ -0,0 +1,25 @@ +title: contains +--- +summary: Checks if an item contains another one. +--- +type: method +--- +signature: item +--- +body: + +This expression evaluates to true if an item is contained within a field. +This works with fields that are lists in nature or [strings +:ref](../../types/string/). For instance a string can be +contained in another string or a item can be contained within a list. + +## Template Example + +```html+jinja +

Projects Tagged 'amazing'

+ +``` diff --git a/content/docs/api/db/expression/contents.lr b/content/docs/api/db/expression/contents.lr new file mode 100644 index 00000000..afc787ec --- /dev/null +++ b/content/docs/api/db/expression/contents.lr @@ -0,0 +1,27 @@ +title: Expression +--- +summary: Represents filter expressions for the query system. +--- +module: lektor.db +--- +type: class +--- +body: + +Expressions are used to filter down [Query :ref](../query/) objects. They +can be passed to the [filter :ref](../query/filter/) function in particular. + +The most basic expression is created by accessing the [F :ref](../f/) object +which will return an expression that points to a field. Further manipulation +of it can create more expressive expressions. `F.name` literally just means +that a field by that name exists and is set to a value. + +The query syntax is mostly the same in Python as well as in the Jinja 2 +templates, the main difference are [and :ref](../and/) and [or :ref](../or/). + +## Example + +```pycon +>>> p.children.filter((F.name == 'foo') | (F.name == 'bar')).all() +[, ] +``` diff --git a/content/docs/api/db/expression/endswith-cs/contents.lr b/content/docs/api/db/expression/endswith-cs/contents.lr new file mode 100644 index 00000000..e60b62bc --- /dev/null +++ b/content/docs/api/db/expression/endswith-cs/contents.lr @@ -0,0 +1,24 @@ +title: endswith_cs +--- +summary: True if a string ends with another string (case sensitive). +--- +type: method +--- +signature: other +--- +body: + +This evaluates to true if the [string :ref](../../types/string/) +on the left side ends with the string on the right side. This method +operates in a case-sensitive manner. For the case-insensitive method +see [endswith :ref](../endswith/). + +## Example + +```html+jinja +
    +{% for item in this.children.filter(F.name.endswith_cs('House')) %} +
  • {{ item.name }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/endswith/contents.lr b/content/docs/api/db/expression/endswith/contents.lr new file mode 100644 index 00000000..b022220c --- /dev/null +++ b/content/docs/api/db/expression/endswith/contents.lr @@ -0,0 +1,24 @@ +title: endswith +--- +summary: True if a string ends with another string (case insensitive). +--- +type: method +--- +signature: other +--- +body: + +This evaluates to true if the [string :ref](../../types/string/) +on the left side ends with the string on the right side. This method +operates in a case-insensitive manner. For the case-sensitive method +see [endswith_cs :ref](../endswith-cs/). + +## Example + +```html+jinja +
    +{% for item in this.children.filter(F.name.endswith('house')) %} +
  • {{ item.name }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/eq/contents.lr b/content/docs/api/db/expression/eq/contents.lr new file mode 100644 index 00000000..ec8e68bb --- /dev/null +++ b/content/docs/api/db/expression/eq/contents.lr @@ -0,0 +1,21 @@ +title: == +--- +summary: Compares an expression to another. +--- +type: operator +--- +body: + +This checks if the left side of the expression matches the right side. +Typically it compares if a value matches a specific value exactly: + +## Example + +```html+jinja +

Our Houses

+
    +{% for project in this.children.filter(F.type == 'house') %} +
  • {{ project.name }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/ge/contents.lr b/content/docs/api/db/expression/ge/contents.lr new file mode 100644 index 00000000..834400ea --- /dev/null +++ b/content/docs/api/db/expression/ge/contents.lr @@ -0,0 +1,22 @@ +title: >= +--- +summary: True if the left side is larger or equal to the right side. +--- +type: operator +--- +body: + +This evalutes to true if the left side compares larger than the right side +or equal to it. This behavior works best with [integers +:ref](../../types/integer/) or [floats :ref](../../types/float/). + +## Template Example + +```html+jinja +

3 or more Stars

+
    +{% for item in this.children.filter(F.stars >= 3) %} +
  • {{ item.name }}: {{ item.stars }} stars +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/gt/contents.lr b/content/docs/api/db/expression/gt/contents.lr new file mode 100644 index 00000000..ff5f6d6d --- /dev/null +++ b/content/docs/api/db/expression/gt/contents.lr @@ -0,0 +1,22 @@ +title: > +--- +summary: True if the left side is larger than the right side. +--- +type: operator +--- +body: + +This evalutes to true if the left side compares larger than the right side. +This behavior works best with [integers :ref](../../types/integer/) +or [floats :ref](../../types/float/). + +## Template Example + +```html+jinja +

Well Rated Items

+
    +{% for item in this.children.filter(F.stars > 3) %} +
  • {{ item.name }}: {{ item.stars }} stars +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/le/contents.lr b/content/docs/api/db/expression/le/contents.lr new file mode 100644 index 00000000..c52b76f9 --- /dev/null +++ b/content/docs/api/db/expression/le/contents.lr @@ -0,0 +1,22 @@ +title: <= +--- +summary: True if the left side is smaller or equal to the right side. +--- +type: operator +--- +body: + +This evalutes to true if the left side compares smaller than the right side +or equal to it. This behavior works best with [integers +:ref](../../types/integer/) or [floats :ref](../../types/float/). + +## Template Example + +```html+jinja +

Projects From Before Including 2000

+
    +{% for item in this.children.filter(F.year <= 2000) %} +
  • {{ item.name }} ({{ item.year }}) +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/lt/contents.lr b/content/docs/api/db/expression/lt/contents.lr new file mode 100644 index 00000000..ebdd1543 --- /dev/null +++ b/content/docs/api/db/expression/lt/contents.lr @@ -0,0 +1,22 @@ +title: < +--- +summary: True if the left side is smaller than the right side. +--- +type: operator +--- +body: + +This evalutes to true if the left side compares smaller than the right side +or equal to it. This behavior works best with [integers +:ref](../../types/integer/) or [floats :ref](../../types/float/). + +## Template Example + +```html+jinja +

Projects From Before 2000

+
    +{% for item in this.children.filter(F.year < 2000) %} +
  • {{ item.name }} ({{ item.year }}) +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/ne/contents.lr b/content/docs/api/db/expression/ne/contents.lr new file mode 100644 index 00000000..97b3abd2 --- /dev/null +++ b/content/docs/api/db/expression/ne/contents.lr @@ -0,0 +1,21 @@ +title: != +--- +summary: Compares an expression to another by ensuring inequality. +--- +type: operator +--- +body: + +This checks if the left side of the expression does not match the right side +by doing a exact comparision: + +## Example + +```html+jinja +

Everything Other Than Houses

+
    +{% for project in this.children.filter(F.type != 'house') %} +
  • {{ project.name }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/or/contents.lr b/content/docs/api/db/expression/or/contents.lr new file mode 100644 index 00000000..7851cf7e --- /dev/null +++ b/content/docs/api/db/expression/or/contents.lr @@ -0,0 +1,34 @@ +title: or +--- +summary: True if either left and right side are true. +--- +type: method +--- +signature: other +--- +body: + +This evaluates to true if either the expression on the left or the +expression on the right are true. This is one of the few operators that +differs between Python and templates. In templates you have to use the +`or` method whereas in Python have to use the `|` operator. + +## Template Example + +```html+jinja +

Hotels or Apartments

+
    +{% for item in this.children.filter( + (F.type == 'hotel').or(F.type == 'apartment')) %} +
  • {{ item.name }} ({{ item.type}}) +{% endfor %} +
+``` + +## Python Example + +```python +def get_hotels_or_apartments(page): + return page.children.filter( + (F.type == 'hotel') | (F.type == 'apartment')) +``` diff --git a/content/docs/api/db/expression/startswith-cs/contents.lr b/content/docs/api/db/expression/startswith-cs/contents.lr new file mode 100644 index 00000000..22ce74e3 --- /dev/null +++ b/content/docs/api/db/expression/startswith-cs/contents.lr @@ -0,0 +1,25 @@ +title: startswith_cs +--- +summary: True if a string starts with another string (case sensitive). +--- +type: method +--- +signature: other +--- +body: + +This evaluates to true if the [string :ref](../../types/string/) +on the left side starts with the string on the right side. This method +operates in a case-sensitive manner. For the case-insensitive method +see [startswith :ref](../startswith/). + +## Example + +```html+jinja +

A

+
    +{% for item in this.children.filter(F.name.startswith_cs('A')) %} +
  • {{ item.name }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/expression/startswith/contents.lr b/content/docs/api/db/expression/startswith/contents.lr new file mode 100644 index 00000000..beddd1ab --- /dev/null +++ b/content/docs/api/db/expression/startswith/contents.lr @@ -0,0 +1,25 @@ +title: startswith +--- +summary: True if a string starts with another string (case insensitive). +--- +type: method +--- +signature: other +--- +body: + +This evaluates to true if the [string :ref](../../types/string/) +on the left side starts with the string on the right side. This method +operates in a case-insensitive manner. For the case-sensitive method +see [startswith_cs :ref](../startswith-cs/). + +## Example + +```html+jinja +

A

+
    +{% for item in this.children.filter(F.name.startswith('a')) %} +
  • {{ item.name }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/f/contents.lr b/content/docs/api/db/f/contents.lr new file mode 100644 index 00000000..c5550773 --- /dev/null +++ b/content/docs/api/db/f/contents.lr @@ -0,0 +1,28 @@ +title: F +--- +summary: Helper object to refer to fields in query filters. +--- +module: lektor.db +--- +template_var: F +--- +type: class +--- +body: + +When filtering a [Query :ref](../query/) it's often necessary to refer to a +field of an arbitrary record. This can be achieved with the `F` object. Any +attribute of it refers to a field in the record. To make this clearer, have a +look at the example below. + +Accessing an attributes creates an [Expression :ref](../expression/). + +## Example + +```html+jinja +
    +{% for item in this.children.filter(F.status == 'published') %} +
  • {{ item.title }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/obj/alt/contents.lr b/content/docs/api/db/obj/alt/contents.lr new file mode 100644 index 00000000..3dd64e46 --- /dev/null +++ b/content/docs/api/db/obj/alt/contents.lr @@ -0,0 +1,17 @@ +title: alt +--- +summary: The alt the source object corresponds to if available. +--- +type: property +--- +body: + +For most source records there will be an associated [alt +:ref](../../../../content/alts/). This attribute points to it. If the content +is not associated with an alt this can be `None`. + +## Example + +```html+jinja + +``` diff --git a/content/docs/api/db/obj/contents.lr b/content/docs/api/db/obj/contents.lr new file mode 100644 index 00000000..c41e3d70 --- /dev/null +++ b/content/docs/api/db/obj/contents.lr @@ -0,0 +1,50 @@ +title: SourceObject +--- +summary: The basic interface of all source objects. +--- +module: lektor.db +--- +type: class +--- +body: + +Source objects is the common interface for all things that come out of the +database. There are two types of source objects: + +* [Records :ref](../record/) which are pages and attachments from the + `contents/` folder. +* [Assets :ref](../asset/) which are files and directories from the + `assets/` folder. + +Whatever object you have in hands that comes from the database, they will +at least provide a minimal useful set of API methods and properties. + +Plugins can subclass source objects to come up with their own source +objects if needed. In addition to that there is a special source object +called the `VirtualSourceObject` which is more useful for plugin usage. + +## Virtual Source Objects + +Most plugins will not have source objects that actually originate on the +file system. This means that their "source" is entirely virtual. Because +this is a very common situation there is a base class, the +`VirtualSourceObject` which plugins can subclass. The constructor takes one +argument which is the parent source object the virtual source lives below. + +```python +from lektor.sourceobj import VirtualSourceObject + +class Source(VirtualSourceObject): + + @property + def source_content(self): + with open(self.parent.source_filename) as f: + return f.read().decode('utf-8') + + @property + def url_path(self): + return self.parent.url_path + 'source.txt' +``` + +For more information see [add-build-program +:ref](../../environment/add-build-program/). diff --git a/content/docs/api/db/obj/is-child-of/contents.lr b/content/docs/api/db/obj/is-child-of/contents.lr new file mode 100644 index 00000000..df86afe9 --- /dev/null +++ b/content/docs/api/db/obj/is-child-of/contents.lr @@ -0,0 +1,22 @@ +title: is_child_of +--- +summary: Indicates if the record is a page or attachment. +--- +signature: path, strict=False +--- +type: method +--- +body: + +This method is a convenient way to check if a page is a child of another +page or not. The default behavior is to consider a page to be a child of +itself as this is more convenient in most situations but this can be +changed with the `strict` parameter. This method is particularly useful +when building a navigation. + +## Example + +```html+jinja +Projects +``` diff --git a/content/docs/api/db/obj/is-hidden/contents.lr b/content/docs/api/db/obj/is-hidden/contents.lr new file mode 100644 index 00000000..68a3385e --- /dev/null +++ b/content/docs/api/db/obj/is-hidden/contents.lr @@ -0,0 +1,29 @@ +title: is_hidden +--- +summary: Indicates if the object is hidden or not. +--- +type: property +--- +body: + +Records can be hidden by setting the default `_hidden` system field to +`yes`. However records also get hidden automatically if any of their parents +are hidden. This property can quickly check if a record is considered hidden +by Lektor or not. + +Hidden records can be queried but final pages will not be built. This is +useful for pages that should only exist for assisting other pages. For +instance hidden pages can be used to store configuration values. + +This property is implemented on the level of source objects to make it +possible to use this API in all cases though the default implementation for +source objects is that they are always visible. + +## Example + +```html+jinja +{% set downloads = site.get('/downloads') %} +{% if downloads.is_hidden %} +

Downloads are currently unavailable +{% endif %} +``` diff --git a/content/docs/api/db/obj/is-visible/contents.lr b/content/docs/api/db/obj/is-visible/contents.lr new file mode 100644 index 00000000..1b4526d1 --- /dev/null +++ b/content/docs/api/db/obj/is-visible/contents.lr @@ -0,0 +1,18 @@ +title: is_visible +--- +summary: Indicates if the object is visible or not. +--- +type: property +--- +body: + +This is exactly the opposite of [is_hidden :ref](../is-hidden/). + +## Example + +```html+jinja +{% set downloads = site.get('/downloads') %} +{% if downloads.is_visible %} +

go to downloads +{% endif %} +``` diff --git a/content/docs/api/db/obj/parent/contents.lr b/content/docs/api/db/obj/parent/contents.lr new file mode 100644 index 00000000..65aa5cc2 --- /dev/null +++ b/content/docs/api/db/obj/parent/contents.lr @@ -0,0 +1,18 @@ +title: parent +--- +summary: The parent object for this source object. +--- +type: property +--- +body: + +For most source objects it's possible to discover their parents through +this property. It's not a requirement that this property is implemented but +most will have it. In particular it's useful for virtual source objects +where this property can be used to discover the associated parent object. + +## Example + +```html+jinja +

My parent is: {{ this.parent.path }} +``` diff --git a/content/docs/api/db/obj/path/contents.lr b/content/docs/api/db/obj/path/contents.lr new file mode 100644 index 00000000..d3c39852 --- /dev/null +++ b/content/docs/api/db/obj/path/contents.lr @@ -0,0 +1,13 @@ +title: path +--- +summary: The path in the source tree of the source object. +--- +type: property +--- +body: + +For source objects that have a file system representation this resolves to +the path that can be used to uniquely query the object via the database. + +For some source objects (like assets or source objects from plugins) the path +will be `None` as the object cannot be queried (it's virtual). diff --git a/content/docs/api/db/obj/source-filename/contents.lr b/content/docs/api/db/obj/source-filename/contents.lr new file mode 100644 index 00000000..5df202eb --- /dev/null +++ b/content/docs/api/db/obj/source-filename/contents.lr @@ -0,0 +1,20 @@ +title: source_filename +--- +summary: Returns the filename of the source file. +--- +type: property +--- +body: + +This returns the primary source filename of the source object. So for +instance if this is an asset it will be the path to it, if it is a record +it will be the path to the lektor contents file. The path is relative +to the project folder. + +## Example + +```html+jinja +

+ Generated from {{ this.source_filename }} +

+``` diff --git a/content/docs/api/db/obj/url-path/contents.lr b/content/docs/api/db/obj/url-path/contents.lr new file mode 100644 index 00000000..0343f24e --- /dev/null +++ b/content/docs/api/db/obj/url-path/contents.lr @@ -0,0 +1,11 @@ +title: url_path +--- +summary: The absolute URL path of the source object. +--- +type: property +--- +body: + +This is the absolute URL path to the page where the source object can be +reached when rendered. Typically the [url :ref](../../../templates/filters/url/) +filter or the [url_to :ref](../url-to/) method would be used. diff --git a/content/docs/api/db/obj/url-to/contents.lr b/content/docs/api/db/obj/url-to/contents.lr new file mode 100644 index 00000000..41518bec --- /dev/null +++ b/content/docs/api/db/obj/url-to/contents.lr @@ -0,0 +1,27 @@ +title: url_to +--- +summary: Generates a URL relative to another path. +--- +type: method +--- +signature: path, alt=None, absolute=False, external=False +--- +body: + +Calculates the URL from the current source object to the given other source +object. Alternatively a path can also be provided instead of a source object. +If the path starts with a leading bang (``!``) then no resolving is performed. +If no `alt` is provided the `alt` of the page is used. + +This is what the `|url` filter uses internally to generate URLs. + +In addition to that `absolute` can enforce the URL to be absolute instead of +relative to the current page and `external` can be used to also add the +domain part to the URL (if configured). + +## Example + +```html+jinja +{% set downloads = site.get('/downloads') %} +Path from downloads to here: {{ downloads.url_to(this) }} +``` diff --git a/content/docs/api/db/pad/contents.lr b/content/docs/api/db/pad/contents.lr new file mode 100644 index 00000000..8c7f1d77 --- /dev/null +++ b/content/docs/api/db/pad/contents.lr @@ -0,0 +1,69 @@ +title: Pad +--- +summary: The basic interface to querying the database. +--- +module: lektor.db +--- +type: class +--- +template_var: site +--- +body: + +The `Pad` is a helper class that provides basic access to querying the +database. Inside templates an instance of it is available under the +`site` variable automatically. + +To understand what the pad does you need to consider what it does. The +pad is similar to a `connection` in a regular database. Whenever you +load a record it's temporarily cached on the pad. So unless you overflow +the cache, loading the object a second time will most likely return an +already loaded instance from the pad. This also means that the pad +is not threadsafe. So if you want (for whatever reason) use multiple +threads you have to create a separate pad for each thread. + +This typically only comes up in the context of plugins or more complex +command line scripts. + +## Template Usage + +A ready-configured pad is always available under the `site` name which +allows you to easily discover other pages. Here a basic example of +how to do this through the [get :ref](get/) method: + +```html+jinja +{% set root = site.get('/') %} +{{ this.title }} | {{ root.title }} +``` + +## Plugin Usage + +Within plugins it's typically not a good idae to construct a new Pad. +Instead you can get access to the current pad from the active context: + +```python +from lektor.context import get_ctx + +ctx = get_ctx() +if ctx is not None: + pad = get_ctx().pad +``` + +Note that you can only get access to a pad if a context is available. This +will not be the case when the plugin is currently being initialized for +instance. + +## Manual Pad Creation + +If you want to work with the database from a script, you can create a +pad from the [Environment :ref](../../environment/) with the help of +the [new_pad :ref](../../environment/new-pad/) method: + +```python +from lektor.project import Project + +project = Project.discover() +env = project.make_env() +pad = env.new_pad() +root_record = pad.root +``` diff --git a/content/docs/api/db/pad/databags/contents.lr b/content/docs/api/db/pad/databags/contents.lr new file mode 100644 index 00000000..8bbd0a5e --- /dev/null +++ b/content/docs/api/db/pad/databags/contents.lr @@ -0,0 +1,9 @@ +title: databags +--- +summary: Provides access to the databags system. +--- +type: property +--- +body: + +Through this attribute you can access the stored [databags :ref](../../../databags/). diff --git a/content/docs/api/db/pad/get-root/contents.lr b/content/docs/api/db/pad/get-root/contents.lr new file mode 100644 index 00000000..c29db5b7 --- /dev/null +++ b/content/docs/api/db/pad/get-root/contents.lr @@ -0,0 +1,21 @@ +title: get_root +--- +summary: Returns the root object of a specific alternative. +--- +type: method +--- +signature: alt='_primary' +--- +body: + +The tree only has one root record but it can come in different +[Alternatives :ref](../../../../content/alts/). This method can be used +to target the root page of a specific one. If no alt is provided, then the +primary alt is loaded. + +## Example + +```html+jinja +{% set root = site.get_root(alt='de') %} +Go to German Page +``` diff --git a/content/docs/api/db/pad/get/contents.lr b/content/docs/api/db/pad/get/contents.lr new file mode 100644 index 00000000..43eb15e7 --- /dev/null +++ b/content/docs/api/db/pad/get/contents.lr @@ -0,0 +1,36 @@ +title: get +--- +summary: Loads a single record. +--- +type: method +--- +signature: id, alt='_primary', page_num=None +--- +body: + +This can look up a single [Record :ref](../../record/) and return it. In templates this method +is particularly useful when having to work with other pages that are well +known to the system but some information should be pulled from. Note that +this loads by default the "primary" [Alternative :ref](../../../../content/alts/). + +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 +want to select a specific page for pagination, then you need to pass +`page_num` with a valid page number. + +## Examples + +This is a simple example that shows how to use the method in a template: + +```html+jinja +{% set root = site.get('/') %} +{{ this.title }} | {{ root.title }} +``` + +Here another example that loads the current page but in another language: + +```html+jinja +{% set other_lang = site.get(this._path, alt='ru') %} +

This page in Russian: {{ other_lang.title }} +``` diff --git a/content/docs/api/db/pad/query/contents.lr b/content/docs/api/db/pad/query/contents.lr new file mode 100644 index 00000000..7319a7f8 --- /dev/null +++ b/content/docs/api/db/pad/query/contents.lr @@ -0,0 +1,24 @@ +title: query +--- +summary: Creates a query at a specific path. +--- +type: method +--- +signature: path=None, alt='_primary' +--- +body: + +This is one of the many ways to create a [Query :ref](../../query/) object in +Lektor. It creates a query at a specific path and alt. This is an +alternative to accessing the children of the [root :ref](../root/) record +and will also include hidden pages. + +## Example + +```html+jinja +

    +{% for project in site.query('/projects') %] +
  • {{ project.name }}: {{ project.year }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/pad/resolve-url-path/contents.lr b/content/docs/api/db/pad/resolve-url-path/contents.lr new file mode 100644 index 00000000..016df602 --- /dev/null +++ b/content/docs/api/db/pad/resolve-url-path/contents.lr @@ -0,0 +1,33 @@ +title: resolve_url_path +--- +summary: Resolves a URL path to a into a source object. +--- +type: method +--- +signature: url_path, include_invisible=False, include_assets=False, alt_fallback=True +--- +body: + +This method is used by Lektor to resolve a URL path to a +[Source Object :ref](../../obj/). This is not particularly useful to use +within templates but it's very useful in the `shell` to debug what's +happening in Lektor. + +This can resolve into any source object, so not just records. If you only +want to resolve to records you can pass `include_assets=False` and only +records will be included. + +## Examples + +Shows an example of how to resolve paths into assets: + +```pycon +>>> pad.resolve_url_path('/docs/api') + +>>> pad.resolve_url_path('/header.jpg') + +>>> pad.resolve_url_path('/static') + +>>> pad.resolve_url_path('/missing-page') is None +True +``` diff --git a/content/docs/api/db/pad/root/contents.lr b/content/docs/api/db/pad/root/contents.lr new file mode 100644 index 00000000..e2dd18c2 --- /dev/null +++ b/content/docs/api/db/pad/root/contents.lr @@ -0,0 +1,16 @@ +title: root +--- +summary: Returns the root object of the primary alternative. +--- +type: property +--- +body: + +This works exactly like [get_root :ref](../get-root/) but always returns +the primary alternative and is implemented as a property. + +## Example + +```html+jinja +Go to Index +``` diff --git a/content/docs/api/db/query/all/contents.lr b/content/docs/api/db/query/all/contents.lr new file mode 100644 index 00000000..311fd4bc --- /dev/null +++ b/content/docs/api/db/query/all/contents.lr @@ -0,0 +1,25 @@ +title: all +--- +summary: Returns all matching records as list. +--- +type: method +--- +body: + +This method returns all matching [Records :ref](../../record/) that match +the query as a list. In many cases just iterating over the query achieve +the same result, but if you want an actual list this method comes in +handy. + +## Example + +```html+jinja +{% items = site.query('/projects').include_hidden(false).all() %} +{% if items %} +
    + {% for item in items %} +
  • {{ item.name }} + {% endfor %} +
+{% endif %} +``` diff --git a/content/docs/api/db/query/contents.lr b/content/docs/api/db/query/contents.lr new file mode 100644 index 00000000..51b351a2 --- /dev/null +++ b/content/docs/api/db/query/contents.lr @@ -0,0 +1,28 @@ +title: Query +--- +summary: Provides functionality to query the database. +--- +module: lektor.db +--- +type: class +--- +body: + +The query class is used to filter queries to the database. It's available +through either the [Pad :ref](../pad/) or through things like the +[children :ref](../record/children/) property of records. + +Most operations on a query object return another one which will return a +more filtered result. + +## Example + +Here a basic example of how to filter something in a template: + +```html+jinja +
    +{% for item in this.children.filter(F.status == 'published') %} +
  • {{ item.title }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/query/count/contents.lr b/content/docs/api/db/query/count/contents.lr new file mode 100644 index 00000000..d302825a --- /dev/null +++ b/content/docs/api/db/query/count/contents.lr @@ -0,0 +1,16 @@ +title: count +--- +summary: Counts the total number of records a query matches. +--- +type: method +--- +body: + +This is a simple way to count the total number of items a query matches. + +## Example + +```html+jinja +{% set project_count = site.query('/projects').count() %} +

We built {{ project_count }} projects. +``` diff --git a/content/docs/api/db/query/filter/contents.lr b/content/docs/api/db/query/filter/contents.lr new file mode 100644 index 00000000..0d62f97d --- /dev/null +++ b/content/docs/api/db/query/filter/contents.lr @@ -0,0 +1,28 @@ +title: filter +--- +summary: Filters a query down by an expression. +--- +type: method +--- +signature: expr +--- +body: + +This filters a query further down by an [Expression :ref](../../expr/). +Most expressions involve filtering by fields through [F :ref](../../f/) +which allows you to perform comparisions with values that fields have. + +Multiple filter calls can be chained as an alternative to using an `and` +expression. + +## Example + +Here a basic example of how to filter something in a template: + +```html+jinja +

    +{% for item in this.children.filter(F.status == 'published') %} +
  • {{ item.title }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/query/first/contents.lr b/content/docs/api/db/query/first/contents.lr new file mode 100644 index 00000000..0ac23b37 --- /dev/null +++ b/content/docs/api/db/query/first/contents.lr @@ -0,0 +1,20 @@ +title: first +--- +summary: Returns the first matching record. +--- +type: method +--- +body: + +This method returns the first [Record :ref](../../record/) that matches the +query. If no such record can be produced, `None` is returned. + +## Example + +```html+jinja +{% first_visible = this.children.first() %} +{% if first_visible %} +

Explore More ...

+

{{ first_visible.title }} +{% endif %} +``` diff --git a/content/docs/api/db/query/get/contents.lr b/content/docs/api/db/query/get/contents.lr new file mode 100644 index 00000000..baacd54a --- /dev/null +++ b/content/docs/api/db/query/get/contents.lr @@ -0,0 +1,22 @@ +title: get +--- +summary: Looks up an item by ID. +--- +type: method +--- +signature: id, page_num=... +--- +body: + +This method mostly exists for consistency. It's an alternative to using +the [get :ref](../../pad/get/) method of the pad but respects the currently +applied filtering. If `page_num` is provided it overrides the currently +requested page number from the query for pagination. + +## Example + +```html+jinja +{% set p1 = this.children.get('project-1') %} +

{{ p1.name }}

+

Our favorite! +``` diff --git a/content/docs/api/db/query/include-hidden/contents.lr b/content/docs/api/db/query/include-hidden/contents.lr new file mode 100644 index 00000000..eeea56a2 --- /dev/null +++ b/content/docs/api/db/query/include-hidden/contents.lr @@ -0,0 +1,29 @@ +title: include_hidden +--- +summary: Changes the query so that it includes or does not include hidden records. +--- +type: method +--- +signature: value +--- +body: + +This controls how the query should behave with regards to hidden records. +A query created from the [children :ref](../../record/children/) attribute of +a record will not include hidden records by default. The opposite is true +for queries created from the [query :ref](../../pad/query/) method of the pad. + +The parameter can be set to `True` to include hidden or `False` to exclude +hidden records. + +## Example + +Here a basic example of how to filter something in a template: + +```html+jinja +

    +{% for item in this.children.include_hidden(true) %} +
  • {{ item.title }}{% if item.is_hidden %} (hidden){% endif %} +{% endfor %} +
+``` diff --git a/content/docs/api/db/query/limit/contents.lr b/content/docs/api/db/query/limit/contents.lr new file mode 100644 index 00000000..3229a7c7 --- /dev/null +++ b/content/docs/api/db/query/limit/contents.lr @@ -0,0 +1,23 @@ +title: limit +--- +summary: Limits the total number of items returned. +--- +type: method +--- +signature: offset +--- +body: + +The offset method can be used to limit the return value to a certain number +of matching records. + +## Example + +```html+jinja +

Our Top 3

+
    +{% for item in this.children.order_by('-rating').limit(3) %} +
  • {{ item.title }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/query/offset/contents.lr b/content/docs/api/db/query/offset/contents.lr new file mode 100644 index 00000000..061f2141 --- /dev/null +++ b/content/docs/api/db/query/offset/contents.lr @@ -0,0 +1,22 @@ +title: offset +--- +summary: Skips a certain number of records. +--- +type: method +--- +signature: offset +--- +body: + +The offset method can be used to skip a certain number of items. Typically +this is not useful as pagination comes built-in, but it can be helpful in +some manual scenarios when working with data from the `shell`. + +This is typically combined with [limit :ref](../limit/). + +## Example + +```pycon +>>> pad.query('/projects').limit(5).offset(10).all() +[...] +``` diff --git a/content/docs/api/db/query/order-by/contents.lr b/content/docs/api/db/query/order-by/contents.lr new file mode 100644 index 00000000..c93c1dfb --- /dev/null +++ b/content/docs/api/db/query/order-by/contents.lr @@ -0,0 +1,29 @@ +title: order_by +--- +summary: Changes the ordering of the query. +--- +type: method +--- +signature: *fields +--- +body: + +This is a handy way to change the order of the items returned. The default +order is defined in the model config but can be overriden this way. The +method accepts an arbitrary number of strings, each of which refers to the +name of a field. If a string is prefixed with a minus sign (`-`) then the +order is reversed. + +If two records have the same value for a field, then the ordering is defined on +the next argument given. So if you order by (`'year', 'name'`) it will first +order by year and within a year it will order by name. + +## Example + +```html+jinja +
    +{% for project in this.children.order_by('-year', 'name') %} +
  • {{ project.year }}: {{ project.name }} +{% endif %} +
+``` diff --git a/content/docs/api/db/query/request-page/contents.lr b/content/docs/api/db/query/request-page/contents.lr new file mode 100644 index 00000000..64fa975f --- /dev/null +++ b/content/docs/api/db/query/request-page/contents.lr @@ -0,0 +1,30 @@ +title: request_page +--- +summary: Requests a specific page from a pagination. +--- +type: method +--- +signature: page_num +--- +body: + +When pagination is enabled it's often quite useful to select a specific +page of a record that is paginated. When dealing with child records the +default behavior is to return an unpaginated version of the record. With +this parameter it's possible to change this. The parameter is the number +of the page to access. + +Truth be told: this method exists mostly for consistency and less because +there is a good reason to use it. + +## Example + +```html+jinja +
    +{% for child in this.children.request_page(1) %} +
  • + Items on the first page of {{ child.title }}: + {{ child.pagination.items.count() }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/query/self/contents.lr b/content/docs/api/db/query/self/contents.lr new file mode 100644 index 00000000..ed0d8937 --- /dev/null +++ b/content/docs/api/db/query/self/contents.lr @@ -0,0 +1,19 @@ +title: self +--- +summary: Returns the item at the query itself. +--- +type: property +--- +body: + +Because a query object filters "downwards" there this is a way access the item +at the level of the query object. It's particularly useful for debugging +in the `shell`: + +## Example + +```pycon +>>> root = pad.root +>>> root.children.query.self == root +True +``` diff --git a/content/docs/api/db/record/attachments/contents.lr b/content/docs/api/db/record/attachments/contents.lr new file mode 100644 index 00000000..e786577d --- /dev/null +++ b/content/docs/api/db/record/attachments/contents.lr @@ -0,0 +1,21 @@ +title: attachments +--- +summary: Returns a query to the attachments of a page. +--- +type: property +--- +body: + +This returns a [Query :ref](../../query/) to all attachments of a page. This +query can be further filtered to access specific types of attachments if +needed. + +## Example + +```html+jinja +
+{% for image in this.attachments.images %} + +{% endfor %} +
+``` diff --git a/content/docs/api/db/record/children/contents.lr b/content/docs/api/db/record/children/contents.lr new file mode 100644 index 00000000..0f7969fc --- /dev/null +++ b/content/docs/api/db/record/children/contents.lr @@ -0,0 +1,33 @@ +title: children +--- +summary: Returns a query to the children of a page. +--- +type: property +--- +body: + +Because of Lektor's tree based nature it almost all records can have children +below them. The `children` attribute provides a convenient way to access +those. It returns a [Query :ref](../../query/) object that can be used to +further filter down children. + +Attachments of pages are not considered children even though they technically +are. These are accessible via the [attachments :ref](../attachments/) +property instead which works largely the same. + +If a record is an attachment neither `children` nor `attachments` are +available. + +What's important to know about children is that the default query will +exclude hidden children. This is different from creating a query object +via the [query :ref](../../pad/query/) method of the pad. + +## Example + +```html+jinja +
    +{% for child in this.children %} +
  • {{ child.title }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/record/contents.lr b/content/docs/api/db/record/contents.lr new file mode 100644 index 00000000..eb934c57 --- /dev/null +++ b/content/docs/api/db/record/contents.lr @@ -0,0 +1,30 @@ +title: Record +--- +summary: Specific features of pages and attachments. +--- +module: lektor.db +--- +type: class +--- +body: + +Records are [Source Objects :ref](../obj/) that come from the `content/` +folder and correspond to [Data Models :ref](../../../models/). The provide +a wider range of functionality compared to a standard source object but +they also provide all the functionality a regular source object does. + +In addition to the functionality here, they also expose all the configured +fields. + +## Accessing Data + +Most of the time things will just work as you expect. If you access these +objects from templates they give access to their built-in attributes as well +as custom fields through the attribute syntax. If however a built-in +attribute overlaps with your custom field, you need to access the fields +with the subscript syntax (`[]`): + +```html+jinja +

Built-in Path: {{ obj.path }} +

Path field: {{ obj['path'] }} +``` diff --git a/content/docs/api/db/record/contents/contents.lr b/content/docs/api/db/record/contents/contents.lr new file mode 100644 index 00000000..589d05c6 --- /dev/null +++ b/content/docs/api/db/record/contents/contents.lr @@ -0,0 +1,42 @@ +title: contents +--- +summary: Provides access to the raw contents of a record or attachment. +--- +type: property +--- +body: + +This property provides access to the raw contents of a record. For attachments +this gives access to the contents of the actual attachments, for other records +it gives access to the record's `contents.lr` file. + +It provides many useful attributes and methods to do something with the +contents of that file: + +## Properties + +| Property | Description +| ----------- | -------------- +| `sha1` | The SHA1 hash of the contents as hexadecimal string +| `md5` | The MD5 hash of the contents as hexadecimal string +| `integrity` | A subresource integritry string ([read about it](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)) +| `mimetype` | The guessed mimetype of the object +| `bytes` | The number of bytes for the contents as integer + +## Methods + +| Method | Description +| --------------- | ------------ +| `as_data_url()` | Returns a data URL for the contents. Optionally accepts a different mimetype than the guesed one. +| `as_text()` | Returns the contents of the file as text. UTF-8 is assumed. +| `as_bytes()` | Returns the contents of the file as bytes. +| `as_base64()` | Returns the contents of the file as base64 encoded bytes. +| `open()` | Opens the file for editing. Accepts `r` or `rb` as modes and a second argument which is the file encoding. + +## Example + +Here are some ideas of what you can do with it: + +```html+jinja + +``` diff --git a/content/docs/api/db/record/exif/contents.lr b/content/docs/api/db/record/exif/contents.lr new file mode 100644 index 00000000..c7f3aa8c --- /dev/null +++ b/content/docs/api/db/record/exif/contents.lr @@ -0,0 +1,53 @@ +title: exif +--- +summary: Provides access to the EXIF information of an image. +--- +type: property +--- +body: + +This property gives access to some of the EXIF information that might be +embedded in the picture. Not all pictures might contain that information +and some information might be unavailable. In those cases the attribute +will be `null`. + +In addition the `to_dict()` method can be used to convert the EXIF data +into a dictionary that can be dumped to JSON for instance. + +| Field | Description +| ------------------- | -------------------------- +| `artist` | The name of the photographer +| `copyright` | The embedded copyright message +| `created_at` | The timestamp of the image +| `camera` | Combined name of camera make and model +| `camera_make` | The name of the camera manufacturer +| `camera_model` | The name of the camera model +| `lens` | Combined name of the lense make and model +| `lens_make` | The name of the lense manufacturer +| `lens_model` | The name of the lense model +| `aperture` | The aperture value as floating point number +| `f_num` | The F-number as actual number +| `f` | The f-number as string (`ƒ/2.2` for instance) +| `iso` | The ISO speed as integer +| `exposure_time` | The exposure time as string +| `shutter_speed` | The shutter speed as string +| `focal_length` | The focal length as string +| `focal_length_35mm` | The focal length as string in the 35mm equivalent +| `flash_info` | Information text about the flash usage +| `location` | Longitude and latitude as floating point tuple. +| `latitude` | The longitude as floating point value. +| `longitude` | The longitude as floating point value. +| `altitude` | The altitude in meters as floating point value. + +## Example + +```html+jinja +{% if image.exif.location %} +

Picture Location

+ +{% endif %} +``` diff --git a/content/docs/api/db/record/format/contents.lr b/content/docs/api/db/record/format/contents.lr new file mode 100644 index 00000000..c29479ec --- /dev/null +++ b/content/docs/api/db/record/format/contents.lr @@ -0,0 +1,18 @@ +title: format +--- +summary: Returns the format of a supported attachment. +--- +type: property +--- +body: + +This returns the format of the attachment as a string. This is currently +only implemented for attachments that are images. + +## Example + +```html+jinja +{% for image in this.attachments.images %} +

{{ image._id }}: {{ image.format }} +{% endfor %} +``` diff --git a/content/docs/api/db/record/height/contents.lr b/content/docs/api/db/record/height/contents.lr new file mode 100644 index 00000000..7a0186c7 --- /dev/null +++ b/content/docs/api/db/record/height/contents.lr @@ -0,0 +1,21 @@ +title: height +--- +summary: Returns the height of an image in pixels. +--- +type: property +--- +body: + +If you are dealing with an attachment that is an image, this property becomes +available and indicates the height of the image in pixels if that information +is available. + +## Example + +```html+jinja +{% for image in this.attachments.images %} + +{% endfor %} +``` diff --git a/content/docs/api/db/record/is-attachment/contents.lr b/content/docs/api/db/record/is-attachment/contents.lr new file mode 100644 index 00000000..de6e901f --- /dev/null +++ b/content/docs/api/db/record/is-attachment/contents.lr @@ -0,0 +1,21 @@ +title: is_attachment +--- +summary: Indicates if the record is a page or attachment. +--- +type: property +--- +body: + +This property indicates whether the record is a page or attachment. If it's +an attachment this will be `true`. Because some functionality only exists +on one type but not the other this can be useful in some utility code to +change the behavior. + +## Example + +```html+jinja +{% macro render_record_link(record) %} + {{ record.record_label }} + {% if record.is_attachment %} ({{ record._attachment_type }}){% endif %} +{% endmacro %} +``` diff --git a/content/docs/api/db/record/pagination/contents.lr b/content/docs/api/db/record/pagination/contents.lr new file mode 100644 index 00000000..b71d91ff --- /dev/null +++ b/content/docs/api/db/record/pagination/contents.lr @@ -0,0 +1,61 @@ +title: pagination +--- +summary: Provides access to the pagination information. +--- +type: property +--- +body: + +When pagination is enabled for a page, then `pagination` will return a +pagination controller that gives access to a query that is specific for the +actual page that was navigated to, instead of including all children as well +as information about the current page. In a nutshell: when pagination is +enabled, you should use `pagination.items` instead of `children` on an +overview page. + +The following attributes exist on the pagination object: + +| Attribute | Description +| ----------- | ------------ +| `current` | The current record +| `prev` | The record for the last page (might be `None`) +| `next` | The record for the net page (might be `None`) +| `total` | The total number of items across all pages +| `pages` | The total number of pages +| `has_prev` | `True` if a previous page exists +| `has_next` | `True` if a next page exists +| `items` | The query that resolves to the children of the current page. + +## Item Query Example + +Simple example that shows how to iterate over the items specific to a +page: + +```html+jinja +

    +{% for child in this.pagination.items %} +
  • {{ child.title }} +{% endfor %} +
+``` + +## Pagination Example + +This example shows how to render a basic pagination with a previous and +next page link as well as the number of the current page: + +```html+jinja + +``` diff --git a/content/docs/api/db/record/parent/contents.lr b/content/docs/api/db/record/parent/contents.lr new file mode 100644 index 00000000..94567b3c --- /dev/null +++ b/content/docs/api/db/record/parent/contents.lr @@ -0,0 +1,16 @@ +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 +Up to {{ this.parent.title }} +``` diff --git a/content/docs/api/db/record/record-label/contents.lr b/content/docs/api/db/record/record-label/contents.lr new file mode 100644 index 00000000..319c4d31 --- /dev/null +++ b/content/docs/api/db/record/record-label/contents.lr @@ -0,0 +1,23 @@ +title: record_label +--- +summary: Returns the configured label for this record. +--- +type: property +--- +body: + +Each record in Lektor has a configured Record label that is used by the +admin interface to show a human readable version of it. Typically this is +the title or something similar. This functionality can also be used outside +of the admin panel. This is particularly useful when rendering children of +a page that might use different independent models. + +## Example + +```html+jinja + +``` diff --git a/content/docs/api/db/record/thumbnail/contents.lr b/content/docs/api/db/record/thumbnail/contents.lr new file mode 100644 index 00000000..78b24dfa --- /dev/null +++ b/content/docs/api/db/record/thumbnail/contents.lr @@ -0,0 +1,28 @@ +title: thumbnail +--- +summary: Creates a thumbnail for an image. +--- +type: method +--- +signature: width, height=None +--- +body: + +This method is available on attachments that are images and can be used to +automatically generate a thumbnail. The return value is a thumbnail proxy +that can be either use directly or with the `|url` filter. + +It provides the following attributes: + +* `width`: the thumbnail width in pixels. +* `height`: the thumbnail height in pixels. +* `url_path` the URL path of the thumbnail. This is absolute and needs to + be made relative with the `|url` filter. + +## Example + +```html+jinja +{% for image in this.attachments.images %} + +{% endfor %} +``` diff --git a/content/docs/api/db/record/width/contents.lr b/content/docs/api/db/record/width/contents.lr new file mode 100644 index 00000000..87d251d3 --- /dev/null +++ b/content/docs/api/db/record/width/contents.lr @@ -0,0 +1,21 @@ +title: width +--- +summary: Returns the width of an image in pixels. +--- +type: property +--- +body: + +If you are dealing with an attachment that is an image, this property becomes +available and indicates the width of the image in pixels if that information +is available. + +## Example + +```html+jinja +{% for image in this.attachments.images %} + +{% endfor %} +``` diff --git a/content/docs/api/db/types/boolean/contents.lr b/content/docs/api/db/types/boolean/contents.lr new file mode 100644 index 00000000..593e8148 --- /dev/null +++ b/content/docs/api/db/types/boolean/contents.lr @@ -0,0 +1,40 @@ +title: boolean +--- +type: type +--- +summary: A type that can store either true or false. +--- +body: + +The `boolean` type is a basic checkbox that can be either `true` or `false`. +It's most useful for representing flags that can be enabled or disabled. + +Since it's stored as text, the following values correspond to `true` and +`false`: + +| Considered True | Considered False +| --------------- | ---------------- +| `true` | `false` +| `yes` | `no` +| `1` | `0` + +The `checkbox_label` attribute can be used to give a description to the +checkbox which otherwise looks a little bit lonely in the admin panel. + +## Field Usage + +```ini +[fields.render_big] +label = Render big +type = boolean +checkbox_label = If true, then the page will be rendered larger. +default = false +``` + +## Template Usage + +```html+jinja +
+ ... +
+``` diff --git a/content/docs/api/db/types/checkboxes/contents.lr b/content/docs/api/db/types/checkboxes/contents.lr new file mode 100644 index 00000000..7ad005f7 --- /dev/null +++ b/content/docs/api/db/types/checkboxes/contents.lr @@ -0,0 +1,50 @@ +title: checkboxes +--- +type: type +--- +summary: A type that can be used to select multiple items of a list. +--- +body: + +The `checkboxes` type is a useful type that allows you to select multiple +values in the admin panel from a list of choices. These choices can be +either defined in the model or dynamically loaded from the tree of pages. + +The configuration of the type is a bit more involved because of it's +flexibility. The following options exist: + +* `source`: if provided it should be an expression that evaluates to an + iterable of records. The variable `record` thereby points to the current + record. +* `choices`: instead of `source` this can be provided in which case it is a + comma separated list of values. +* `choice_labels`: when `choices` is given, this is used as labels to display + it in the UI. It's a comma separated list of values. +* `item_key`: an template format expression to convert a source item into + the value that is stored. This defaults to `{{ this._id }}` which means + that the ID of the selected record is stored. +* `item_label`: similar to `item_key` but for the UI part. This has no + default which means that the record's configured title will be used as + label. + +In the contents file the values are stored as comma separated list. + +## Field Usage + +```ini +[fields.slideshow] +label = Slideshow +type = checkboxes +description = Attached images to include in the slidehow +source = record.attachments.images +``` + +## Template Usage + +```html+jinja +{% for image in this.attachments.images %} + {% if image.id in this.slideshow %} + + {% endif %} +{% endfor %} +``` diff --git a/content/docs/api/db/types/contents.lr b/content/docs/api/db/types/contents.lr new file mode 100644 index 00000000..6753c552 --- /dev/null +++ b/content/docs/api/db/types/contents.lr @@ -0,0 +1,9 @@ +title: Field Types +--- +summary: An overview of all the field types supported in Lektor. +--- +body: + +Lektor supports many different types that can be used for data modelling. +Currently it's not possible to extend these but the intention exists to make +this even more configurable from plugins in the future. diff --git a/content/docs/api/db/types/date/contents.lr b/content/docs/api/db/types/date/contents.lr new file mode 100644 index 00000000..a64c7484 --- /dev/null +++ b/content/docs/api/db/types/date/contents.lr @@ -0,0 +1,27 @@ +title: date +--- +type: type +--- +summary: A type that can store a date. +--- +body: + +The `date` type can store a date. Because it knows a bit more about dates +than a plain old `string` some basic operations can be provided in the +templates. + +The canonical format for the type in text form is `YYYY-MM-DD`. + +## Field Usage + +```ini +[fields.pub_date] +label = Publication date +type = date +``` + +## Template Usage + +```html+jinja +

Published: {{ this.pub_date.strftime('%d/%m/%Y') }} +``` diff --git a/content/docs/api/db/types/float/contents.lr b/content/docs/api/db/types/float/contents.lr new file mode 100644 index 00000000..4dbe4c3d --- /dev/null +++ b/content/docs/api/db/types/float/contents.lr @@ -0,0 +1,29 @@ +title: float +--- +type: type +--- +summary: A type that can store floating point numbers. +--- +body: + +The `float` type is similar to the [integer :ref](../integer/) one but it +can store floating points instead of just integer values. + +## Field Usage + +```ini +[fields.percentage] +label = Percentage +type = float +description = Just a percentage of a progress bar. +addon_label = % +``` + +## Template Usage + +```html+jinja +

+ + {{ this.percentage }}% +
+``` diff --git a/content/docs/api/db/types/flow/contents.lr b/content/docs/api/db/types/flow/contents.lr new file mode 100644 index 00000000..d4729b88 --- /dev/null +++ b/content/docs/api/db/types/flow/contents.lr @@ -0,0 +1,78 @@ +title: flow +--- +type: type +--- +summary: A type to build complex pages out of blocks. +--- +body: + +The `flow` type is a special type that allows you to use +[Flow Blocks :ref](../../../../content/flow/) in a page. Blocks are small +items that work like page models but can be used for a specific field. Such +a flow field can hold as many blocks as you want in a well defined order +which allows you to build complex pages out of these blocks. + +Each block typically renders a specific template automatically although this +can be customized. + +For configuration you can define which blocks are allowed by setting the +`flow_blocks` parameter which is a comma separated list of flow blocks +that are allowed. If not defined, all flow blocks become available. + +The text format for flow blocks in the `contents.lr` file looks a bit more +complex because of it's nested nature, but in essence it's this: + +``` +#### name-of-flow-block #### +field-1: value 1 +----- +field-2: value 2 +``` + +Because flow blocks are stored within another field the `---` needs to be +escaped to `----`. Likewise flow blocks within flow blocks would also +require an additional level by adding additional hashes for the name of the +flow block: + +``` +##### nested-flow-block ##### +field-1: value 1 +------ +field-2: value 2 +``` + +In the template the `flow` type automatically renders out all the blocks +within it, but the blocks can be individually accessed through the +`blocks` attribute. Each block's attributes are the individual fields which +you are free to access if so desired. + +## Field Usage + +```ini +[fields.body] +label = Body +type = flow +flow_blocks = text, image +``` + +## Template Usage + +```html+jinja +
+ {{ this.body }} +
+``` + +or more complex: + +```html+jinja +
+ {% for item in this.body.blocks %} +
{{ item }}
+ {% endfor %} +
+``` + +To see how the actual blocks are rendered have a look at the main +[Flow Documentation :ref](../../content/flow/) which covers templating +in detail. diff --git a/content/docs/api/db/types/html/contents.lr b/content/docs/api/db/types/html/contents.lr new file mode 100644 index 00000000..27405a57 --- /dev/null +++ b/content/docs/api/db/types/html/contents.lr @@ -0,0 +1,27 @@ +title: html +--- +type: type +--- +summary: A type that can store raw HTML. +--- +body: + +The `html` type is basically the same as the [text :ref](../text/) type but +in templates it's rendered directly as HTML instead of being escaped. + +It renders as a multi-line input field in the admin interface. + +## Field Usage + +```ini +[fields.tracking_code] +label = Tracking Code +description = raw HTML that is inserted for ad tracking purposes. +type = text +``` + +## Template Usage + +```html+jinja +{{ this.tracking_code }} +``` diff --git a/content/docs/api/db/types/integer/contents.lr b/content/docs/api/db/types/integer/contents.lr new file mode 100644 index 00000000..a09b8518 --- /dev/null +++ b/content/docs/api/db/types/integer/contents.lr @@ -0,0 +1,31 @@ +title: integer +--- +type: type +--- +summary: A type that can store natural numbers. +--- +body: + +The `integer` type is one of the most basic ones in Lektor. It can store +arbitrary natural numbers both negative and positive. It should be used +instead of a string when a real number with such behavior is wanted as it +sorts numbers correctly and the numbers can be manipulated. + +## Field Usage + +```ini +[fields.image_width] +label = Image width +type = integer +description = The intended image width in pixels. +addon_label = px +``` + +## Template Usage + +```html+jinja +a thumbnail of that image +``` diff --git a/content/docs/api/db/types/markdown/contents.lr b/content/docs/api/db/types/markdown/contents.lr new file mode 100644 index 00000000..25ec01a4 --- /dev/null +++ b/content/docs/api/db/types/markdown/contents.lr @@ -0,0 +1,38 @@ +title: markdown +--- +summary: A type that can store and parse Markdown. +--- +type: type +--- +body: + +The `markdown` format is a special form of the [text :ref](../text/) type but +instead of rendering unformatted text, it parses it as +[Markdown :ext](https://en.wikipedia.org/wiki/Markdown) and converts it into +HTML. + +Normally accessing the Markdown field just returns the rendered HTML but +there are some special attributes on it to access more information: + +| Attribute | Explanation +| -------------- | -------------------------------------- +| `html` | the rendered Markdown as HTML string +| `source` | the unprocessed Markdown source + +Additional attributes can become available through the use of plugins. + +## Field Usage + +```ini +[fields.body] +label = Body +type = markdown +``` + +## Template Usage + +```html+jinja +
+ {{ this.body }} +
+``` diff --git a/content/docs/api/db/types/select/contents.lr b/content/docs/api/db/types/select/contents.lr new file mode 100644 index 00000000..6c48e8b3 --- /dev/null +++ b/content/docs/api/db/types/select/contents.lr @@ -0,0 +1,31 @@ +title: select +--- +summary: A type that can be used to select a single item from a list. +--- +type: type +--- +body: + +The `select` type works exactly like the [checkboxes :ref](../checkboxes/) type +but unlike it you can only select a single item. For configuration options +refer directly to the checkboxes type. + +In the contents file the values are stored as a single textual item. + +## Field Usage + +```ini +[fields.class] +label = Class +type = select +choices = full-width, pull-left, pull-right +choice_labels = Full Width, Pull Left, Pull Right +``` + +## Template Usage + +```html+jinja +
+ ... +
+``` diff --git a/content/docs/api/db/types/string/contents.lr b/content/docs/api/db/types/string/contents.lr new file mode 100644 index 00000000..32a78544 --- /dev/null +++ b/content/docs/api/db/types/string/contents.lr @@ -0,0 +1,31 @@ +title: string +--- +summary: A type that can store a single line of text. +--- +type: type +--- +body: + +The `string` type is the most basic of all types as it can store one line of +arbitrary text. It's useful for many places where you want to show a bit of +text without any special formatting (titles, basic summaries and more). + +It renders as a basic input field in the admin interface. + + +## Field Usage + +```ini +[fields.title] +label = Title +type = string +size = large +description = The title of the page +addon_label = [[header]] +``` + +## Template Usage + +```html+jinja +

{{ this.title }}

+``` diff --git a/content/docs/api/db/types/strings/contents.lr b/content/docs/api/db/types/strings/contents.lr new file mode 100644 index 00000000..9691b23b --- /dev/null +++ b/content/docs/api/db/types/strings/contents.lr @@ -0,0 +1,34 @@ +title: strings +--- +summary: A type that can store multiple separate strings. +--- +type: type +--- +body: + +The `strings` type one that is a mixture of the [string :ref](../string/) +and [text :ref](../text/) type. It renders as a multi-line text area in the +admin but the template will see each line of text separately. It's primarily +useful for some advanced scenarios where enumerations and other things +should be rendered. + + +## Field Usage + +```ini +[fields.things_to_buy] +label = Things to buy +type = strings +description = A list of things that would be good to buy +``` + +## Template Usage + +```html+jinja +

Shopping List

+
    +{% for item in this.things_to_buy %} +
  • {{ item }} +{% endfor %} +
+``` diff --git a/content/docs/api/db/types/text/contents.lr b/content/docs/api/db/types/text/contents.lr new file mode 100644 index 00000000..43994eac --- /dev/null +++ b/content/docs/api/db/types/text/contents.lr @@ -0,0 +1,27 @@ +title: text +--- +summary: A type that can store a multiple lines of text. +--- +type: type +--- +body: + +The `text` type is very similar to the [string :ref:](../string/) type but +can store multiple lines. It does not support any formatting but is very +useful for storing code and other things. + +It renders as a multi-line input field in the admin interface. + +## Field Usage + +```ini +[fields.code] +label = Code +type = text +``` + +## Template Usage + +```html+jinja +
{{ this.code }}
+``` diff --git a/content/docs/api/db/types/url/contents.lr b/content/docs/api/db/types/url/contents.lr new file mode 100644 index 00000000..d4df8721 --- /dev/null +++ b/content/docs/api/db/types/url/contents.lr @@ -0,0 +1,41 @@ +title: url +--- +summary: A type that can store URLs. +--- +type: type +--- +body: + +The `url` type is basically a more fancy version of the basic +[string :ref](../string/) type but it performs a basic validation in the +admin UI of being a URL. In addition to that, it gives access to parts +of the URL for better displaying. + +Within templates the URL can be rendered as such, but it also has a few +other attributes that are useful: + +| Attribute | Description +| ------------ | -------------- +| `url` | The URL is a basic string for further manipulation +| `host` | Just the host portion of the URL. +| `port` | Just the port of the URL. +| `path` | The path of the URL +| `query` | The query string of the URL +| `fragment` | The part after the hash symbol (`#`) in the URL +| `scheme` | The URL schema (`http` etc.) +| `ascii_url` | Same as `url` but guaranteed to be ASCII only +| `ascii_host` | Same as `host` but guaranteed to be ASCII only + +## Field Usage + +```ini +[fields.website] +label = Website +type = url +``` + +## Template Usage + +```html+jinja +{{ this.url.host }} +``` diff --git a/content/docs/api/environment/add-build-program/contents.lr b/content/docs/api/environment/add-build-program/contents.lr new file mode 100644 index 00000000..2257da31 --- /dev/null +++ b/content/docs/api/environment/add-build-program/contents.lr @@ -0,0 +1,53 @@ +title: add_build_program +--- +type: method +--- +signature: cls, program +--- +summary: Registers a build program for a source object. +--- +body: + +This is very experimental API and used to register build programs for +custom source objects. This can be used to implement virtual items. This +works in combination with [generator :ref](../generator/) and +[urlresolver :ref](../urlresolver/) and is responsible for generating +artifacts out of source objects. + +## Example + +```python +from lektor.sourceobj import VirtualSourceObject +from lektor.build_programs import BuildProgram + +class Source(VirtualSourceObject): + + @property + def source_content(self): + with open(self.parent.source_filename) as f: + return f.read().decode('utf-8') + + @property + def url_path(self): + return self.parent.url_path + 'source.txt' + +class SourceBuildProgram(BuildProgram): + + def produce_artifacts(self): + self.declare_artifact( + self.source.url_path, + sources=list(self.source.iter_source_filenames())) + + def build_artifact(self, artifact): + artifact.render_template_into('view_source.html', + this=self.source) + +env.add_build_program(Source, SourceBuildProgram) +``` + +And here the example `view_source.html` template: + +```html+jinja +

Source for {{ this.parent.path }}

+
{{ this.source_content }}
+``` diff --git a/content/docs/api/environment/contents.lr b/content/docs/api/environment/contents.lr new file mode 100644 index 00000000..11f8732b --- /dev/null +++ b/content/docs/api/environment/contents.lr @@ -0,0 +1,26 @@ +title: Environment +--- +type: class +--- +module: lektor.environment +--- +summary: The working environment for the build process. +--- +body: + +This class holds all the relevant information for building a Lektor +[Project :ref](../project/). It can be reused between builds and only +holds state that is immutable after initialization. For instance it +will hold all loaded plugins, the configuration for the Jinja 2 +template engine and more. + +Plugins have access to the environment at any point by accessing `self.env`. + +## Example + +```python +from lektor.project import Project + +project = Project.discover() +env = project.make_env() +``` diff --git a/content/docs/api/environment/generator/contents.lr b/content/docs/api/environment/generator/contents.lr new file mode 100644 index 00000000..5b5ef146 --- /dev/null +++ b/content/docs/api/environment/generator/contents.lr @@ -0,0 +1,34 @@ +title: generator +--- +type: method +--- +summary: Registers a custom generator function. +--- +body: + +This is very experimental API and used to register a function that can +generate source objects relative to another one. This works in combination +with [urlresolver :ref](../urlresolver/) but handles the build-all part of +the equation. + +The registered function is invoked for each source after it was build. As +such it's important to only return items if a virtual sub resource actually +exists for a page. + +## Example + +```python +from lektor.sourceobj import VirtualSourceObject +from lektor.db import Record + +class Source(VirtualSourceObject): + + @property + def url_path(self): + return self.parent.url_path + 'source.txt' + +@env.generator +def generate_source_file(node): + if isinstance(node, Record) and not node.is_attachment: + yield Source(node) +``` diff --git a/content/docs/api/environment/jinja-env/contents.lr b/content/docs/api/environment/jinja-env/contents.lr new file mode 100644 index 00000000..69bb64d3 --- /dev/null +++ b/content/docs/api/environment/jinja-env/contents.lr @@ -0,0 +1,33 @@ +title: jinja_env +--- +type: property +--- +summary: The configured Jinja 2 Environment. +--- +body: + +This object is a configured Jinja2 environment. For more information you can +refer to the [Jinja 2 Documentation :ref](http://jinja.pocoo.org/docs/dev/api/#jinja2.Environment). + +This is where plugins can inject additional data like custom filters, tests +or global functions. + +## Plugin Example + +```python +from lektor.pluginsystem import Plugin + +class MyPlugin(Plugin): + ... + + def on_setup_env(self, **extra): + def shout_filter(value): + return unicode(value).upper() + '!!!!1111' + self.env.jinja_env.filters['shout'] = shout_filter +``` + +Then you can use this filter from templates: + +```html+jinja +

{{ page.title|shout }}

+``` diff --git a/content/docs/api/environment/load-config/contents.lr b/content/docs/api/environment/load-config/contents.lr new file mode 100644 index 00000000..3cd5e10e --- /dev/null +++ b/content/docs/api/environment/load-config/contents.lr @@ -0,0 +1,26 @@ +title: load_config +--- +type: method +--- +summary: Loads the current config from the project file. +--- +body: + +Because the environment is reused between builds, the config is not cached +on the environment but needs to be expicitly loaded. This happens with +the help of the `load_config` method. It returns a config object that +gives access to the settings in the project file. + +These settings are work in progress and if you want to know how to use +the config file and what to do with it, you have to consult the source +code. + +## Example + +```python +from lektor.project import Project + +project = Project.discover() +env = project.make_env() +config = env.load_config() +``` diff --git a/content/docs/api/environment/load-plugins/contents.lr b/content/docs/api/environment/load-plugins/contents.lr new file mode 100644 index 00000000..2d193d9e --- /dev/null +++ b/content/docs/api/environment/load-plugins/contents.lr @@ -0,0 +1,21 @@ +title: load_plugins +--- +type: method +--- +summary: Loads the available plugins for the environment. +--- +body: + +Normally when an environment is constructed plugins are loaded automatically. +If you disabled this when creating the environment you can still at a later +point load the plugins by invoking this method. + +## Example + +```python +from lektor.project import Project + +project = Project.discover() +env = project.make_env(load_plugins=False) +env.load_plugins() +``` diff --git a/content/docs/api/environment/new-pad/contents.lr b/content/docs/api/environment/new-pad/contents.lr new file mode 100644 index 00000000..790ec21a --- /dev/null +++ b/content/docs/api/environment/new-pad/contents.lr @@ -0,0 +1,23 @@ +title: new_pad +--- +type: method +--- +summary: Creates a brand new database pad. +--- +body: + +To access the [Database :ref](../../db/) from an environment you need to +first construct a [Pad :ref](../../db/pad/). This method can do that for +you. For more information about how the pad works, you can refer to the +general documentation of it. + +## Example + +```python +from lektor.project import Project + +project = Project.discover() +env = project.make_env() +pad = env.new_pad() +root_record = pad.root +``` diff --git a/content/docs/api/environment/render-template/contents.lr b/content/docs/api/environment/render-template/contents.lr new file mode 100644 index 00000000..4b0e4688 --- /dev/null +++ b/content/docs/api/environment/render-template/contents.lr @@ -0,0 +1,39 @@ +title: render_template +--- +type: method +--- +signature: name, pad=None, this=None, values=None, alt=None +--- +summary: Renders a template from the template folder. +--- +body: + +Whenever Lektor needs to render a template, it will use this exact +method. Here are the parameters and what they mean: + +* `name`: this is the name of the template that should be rendered. It's + the local filename relative to the `templates` folder and uses slashes + for paths. +* `pad`: when a [Pad :ref](../../pad/) is available, it should be provided + so that the `site` variable can be populated. If a context is available + then the pad will also be pulled from the context if needed. +* `this`: the value of the `this` variable in templates. This should always + be the closest renderable thing. Typically this is a [Record + :ref](../../db/record/) or flow block or something similar. +* `values`: optional additional variables can be provided as a dictionary here. +* `alt`: this can override the default selected `alt`. If not provided it's + discovered from `this` and it will default to `_primary` if no other + information can be found. + +## Example + +```python +from lektor.project import Project + +project = Project.discover() +env = project.make_env(load_plugins=False) +pad = env.new_pad() +rv = env.render_template('hello.html', pad=pad, this={ + 'title': 'Demo Object' +}) +``` diff --git a/content/docs/api/environment/urlresolver/contents.lr b/content/docs/api/environment/urlresolver/contents.lr new file mode 100644 index 00000000..b96ee0af --- /dev/null +++ b/content/docs/api/environment/urlresolver/contents.lr @@ -0,0 +1,35 @@ +title: urlresolver +--- +type: method +--- +summary: Registers a custom URL resolver function. +--- +body: + +This is very experimental API and used to register a custom URL resolver +function with the environment. It's invoked whenever a node has matched but +more data is left to parse. This can be used to implement virtual items. +This works in combination with [generator :ref](../generator/) but handles the +URL matching part of the equation. + +The registered function is invoked with a source object and a list of +URL path segments. + +## Example + +```python +from lektor.sourceobj import VirtualSourceObject + +class Source(VirtualSourceObject): + + @property + def url_path(self): + return self.parent.url_path + 'source.txt' + +@env.urlresolver +def match_source_file(node, url_path): + if url_path == ['source.txt'] \ + and isinstance(node, Record) \ + and not node.is_attachment: + return Source(node) +``` diff --git a/content/docs/api/plugins/contents.lr b/content/docs/api/plugins/contents.lr new file mode 100644 index 00000000..7296499c --- /dev/null +++ b/content/docs/api/plugins/contents.lr @@ -0,0 +1,9 @@ +title: Plugins +--- +summary: Provides an introduction to the plugin system in Lektor. +--- +body: + +Lektor allows plugins to depend on plugins to extend functionality. This is +achieved through a flexible and ever growing plugin interface. If you are +new to it have a look at the [Guide :ref](../../plugins/). diff --git a/content/docs/api/plugins/events/contents.lr b/content/docs/api/plugins/events/contents.lr new file mode 100644 index 00000000..d3a14619 --- /dev/null +++ b/content/docs/api/plugins/events/contents.lr @@ -0,0 +1,30 @@ +title: Events +--- +summary: A list of all events emitted by Lektor for plugins. +--- +body: + +As Lektor executes and builds it emits various events that plugins can +respond to. This gives a list of all of those events. + +## Handling Events + +Events are handled by implementing a method with the name of the event and +the prefix `on_` on the plugin and underscores instead of dashes. So for +instance if the event is called [process-template-context +:ref](process-template-context/) the method to implement is +`on_process_template_context`. It's important to additionally *always* +catch extra parameters in a `**extra` parameter so that the plugin does +not break if an event starts sending additional parameters in the future. + +## Emitting Events + +If you want to emit your own events you can do that from the environment's +plugin controller: + +```python +self.emit('your-signal', parameter='value') +``` + +This emits a signal named `your-plugin-your-signal`. The signal prefix is +taken from the plugin ID which is set in `setup.py`. diff --git a/content/docs/api/plugins/events/process-template-context/contents.lr b/content/docs/api/plugins/events/process-template-context/contents.lr new file mode 100644 index 00000000..db5c189d --- /dev/null +++ b/content/docs/api/plugins/events/process-template-context/contents.lr @@ -0,0 +1,34 @@ +title: process-template-context +--- +type: event +--- +signature: context, template=None +--- +summary: An event that can be handled to customize the template context. +--- +body: + +This event is emitted right before a template is rendered and can be used +to inject values directly into the template context. The context is always +provided and is a dictionary that can be modified. + +What's important to know that this is not the only way to modify the template +context. Another one is to modify the underlying [Jinja Environment +:ref](../../../environment/jinja-env/) on the environment directly. The +difference is that the globals from the `jinja_env` are available in *all* +templates whereas the modified template context is only available in the +toplevel template. This becomes apparent when you are dealing with imported +template macros. A macro will not have access to values you provide here +unless they are explicitly passed to it. + +The filename of the template is passed as `template` but is only available +if the template was indeed loaded from the templates folder. There are many +more places where templates are rendered in in those cases the value will +not be provided. + +## Example + +```python +def on_process_template_context(self, context, **extra): + context['my_variable'] = 'my value' +``` diff --git a/content/docs/api/plugins/events/server-spawn/contents.lr b/content/docs/api/plugins/events/server-spawn/contents.lr new file mode 100644 index 00000000..bc5b2873 --- /dev/null +++ b/content/docs/api/plugins/events/server-spawn/contents.lr @@ -0,0 +1,33 @@ +title: server-spawn +--- +type: event +--- +summary: Emitted right after the server was started. +--- +body: + +When the development server boots up it emits this event if plugins want +to spawn their own development tools. For instance it can be used to +start a background process that kicks off webpack or similar tools. There +is a second event called [server-stop :ref](../server-stop/) which can be +used to detect server shutdowns. + +## Example + +```python +import os +from subprocess import Popen + +class MyPlugin(Plugin): + ... + + webpack = None + + def on_server_spawn(self, **extra): + path = os.path.join(self.env.root_path, 'webpack') + self.webpack = Popen(['webpack', '--watch'], cwd=path) + + def on_server_stop(self, **extra): + if self.webpack is not None: + self.webpack.kill() +``` diff --git a/content/docs/api/plugins/events/server-stop/contents.lr b/content/docs/api/plugins/events/server-stop/contents.lr new file mode 100644 index 00000000..91133b2a --- /dev/null +++ b/content/docs/api/plugins/events/server-stop/contents.lr @@ -0,0 +1,10 @@ +title: server-stop +--- +type: event +--- +summary: Emitted right before the server stops. +--- +body: + +This is emitted when the server stops. For more information see +[server-spawn :ref](../server-spawn/) which shows how to use it. diff --git a/content/docs/api/plugins/events/setup-env/contents.lr b/content/docs/api/plugins/events/setup-env/contents.lr new file mode 100644 index 00000000..6b6c0840 --- /dev/null +++ b/content/docs/api/plugins/events/setup-env/contents.lr @@ -0,0 +1,19 @@ +title: setup-env +--- +type: event +--- +summary: Emitted right after the plugin is associated with the environment. +--- +body: + +This event is emitted right after the plugin was initialized and associated +with the [environment :ref](../../../environment/). This can be used to +reconfigure it. For instance this is the perfect place to inject Jinja 2 +filters and global variables. + +## Example + +```python +def on_setup_env(self, **extra): + env.jinja_env.globals['my_variable'] = 'my value' +``` diff --git a/content/docs/api/plugins/get-plugin/contents.lr b/content/docs/api/plugins/get-plugin/contents.lr new file mode 100644 index 00000000..eb037957 --- /dev/null +++ b/content/docs/api/plugins/get-plugin/contents.lr @@ -0,0 +1,25 @@ +title: get_plugin +--- +summary: Look up a plugin instance. +--- +type: function +--- +signature: plugin_id_or_class, env=None +--- +module: lektor.pluginsystem +--- +body: + +This function can look up a plugin instance for an environment by plugin ID +or plugin class. This is particularly useful to retrieve state from a plugin. +If the `env` is not given a context needs to be active and the env of the +context is used. + +## Example + +```python +from lektor.pluginsystem import get_plugin + +plugin = get_plugin('the-plugin-id') +cfg = plugin.get_config() +``` diff --git a/content/docs/api/plugins/plugin/config-filename/contents.lr b/content/docs/api/plugins/plugin/config-filename/contents.lr new file mode 100644 index 00000000..416e51ab --- /dev/null +++ b/content/docs/api/plugins/plugin/config-filename/contents.lr @@ -0,0 +1,36 @@ +title: config_filename +--- +type: property +--- +summary: The filename of the plugin config. +--- +body: + +This property returns the path to the file that holds the config for this +plugin in the loaded project. This is by default in `configs/.ini`. +Plugins could override this in theory but it's not recommended. The primary +use of this property is to track dependencies. + +For a convenient way to load the config see [get_config](../get-config/). + +## Example + +```python +from lektor.pluginsystem import Plugin +from lektor.context import get_ctx + + +class MyPlugin(Plugin): + + def on_env_setup(self, **extra): + color = self.get_config().get('color') + def get_css(artifact_name='/static/demo.css'): + ctx = get_ctx() + @ctx.sub_artifact(artifact_name, sources=[ + self.config_filename]) + def build_stylesheet(artifact): + with artifact.open('w') as f: + f.write('body { background: %s }\n' % color) + return artifact_name + self.env.jinja_env.globals['get_css'] = get_css +``` diff --git a/content/docs/api/plugins/plugin/contents.lr b/content/docs/api/plugins/plugin/contents.lr new file mode 100644 index 00000000..7cc35255 --- /dev/null +++ b/content/docs/api/plugins/plugin/contents.lr @@ -0,0 +1,13 @@ +title: Plugin +--- +type: class +--- +module: lektor.pluginsystem +--- +summary: Documents the functionality of the plugin base class. +--- +body: + +This is the baseclass of all plugins in Lektor. It does not just hold +event handlers and similar things, but also provides some helper methods +that are useful for plugins. diff --git a/content/docs/api/plugins/plugin/description/contents.lr b/content/docs/api/plugins/plugin/description/contents.lr new file mode 100644 index 00000000..eb720b8f --- /dev/null +++ b/content/docs/api/plugins/plugin/description/contents.lr @@ -0,0 +1,21 @@ +title: description +--- +type: property +--- +summary: The human readable plugin description. +--- +body: + +This is a short string with some more explanation of what the plugin does. +It's shown in the UI to give the user some ideas about it's operation. + +## Example + +```python +from lektor.pluginsystem import Plugin + + +class MyPlugin(Plugin): + name = 'My Plugin' + description = 'This is a small plugin I wrote for testing purposes' +``` diff --git a/content/docs/api/plugins/plugin/emit/contents.lr b/content/docs/api/plugins/plugin/emit/contents.lr new file mode 100644 index 00000000..9df3366a --- /dev/null +++ b/content/docs/api/plugins/plugin/emit/contents.lr @@ -0,0 +1,38 @@ +title: emit +--- +type: method +--- +signature: event, **extra +--- +summary: Emits a plugin specific event. +--- +body: + +This method can be used to emit an event that other plugins can hook. The +event name is prefixed with the plugin ID. + +## Example + +```python +from lektor.pluginsystem import Plugin + + +class MyPlugin(Plugin): + + def on_env_setup(self, **extra): + self.emit('setup', foo=42) +``` + +Another plugin can then hook this: + +```python +from lektor.pluginsystem import Plugin + + +class MyPlugin(Plugin): + + def on_my_plugin_setup(self, foo, **extra): + print 'got %s' % foo +``` + +(This assumes the plugin id is set to `my-plugin` in `setup.py`) diff --git a/content/docs/api/plugins/plugin/env/contents.lr b/content/docs/api/plugins/plugin/env/contents.lr new file mode 100644 index 00000000..f45df980 --- /dev/null +++ b/content/docs/api/plugins/plugin/env/contents.lr @@ -0,0 +1,10 @@ +title: env +--- +type: property +--- +summary: A reference to the active environment. +--- +body: + +This is just a reference back to the [Environment :ref](../../../env/) +that the plugin belongs to. diff --git a/content/docs/api/plugins/plugin/get-config/contents.lr b/content/docs/api/plugins/plugin/get-config/contents.lr new file mode 100644 index 00000000..a4634957 --- /dev/null +++ b/content/docs/api/plugins/plugin/get-config/contents.lr @@ -0,0 +1,27 @@ +title: get_config +--- +type: method +--- +signature: fresh=False +--- +summary: Returns a loaded config for the plugin as ini file. +--- +body: + +This loads the config of the plugin as an [Ini File :ext](https://github.com/mitsuhiko/python-inifile). + +Dotted paths are used to navigate into sections. So `get('foo.bar')` would +navigate to the `bar` key in the `[foo]` section. + +## Example + +```python +from lektor.pluginsystem import Plugin + + +class MyPlugin(Plugin): + + def on_env_setup(self, **extra): + color = self.get_config().get('color') + self.env.jinja_env.globals['my_color'] = color +``` diff --git a/content/docs/api/plugins/plugin/name/contents.lr b/content/docs/api/plugins/plugin/name/contents.lr new file mode 100644 index 00000000..231b995f --- /dev/null +++ b/content/docs/api/plugins/plugin/name/contents.lr @@ -0,0 +1,20 @@ +title: name +--- +type: property +--- +summary: The human readable plugin name. +--- +body: + +This property needs to be set to a human readable name for the plugin. It's +what's shown in the UI to show the UI. + +## Example + +```python +from lektor.pluginsystem import Plugin + + +class MyPlugin(Plugin): + name = 'My Plugin' +``` diff --git a/content/docs/api/project/contents.lr b/content/docs/api/project/contents.lr new file mode 100644 index 00000000..ad3c7114 --- /dev/null +++ b/content/docs/api/project/contents.lr @@ -0,0 +1,16 @@ +title: Project +--- +type: class +--- +module: lektor.project +--- +summary: Provides access to a project file. +--- +body: + +The project class is one of the most basic classes that Lektor uses to +implement it's building process. It's generated very early on when the +application needs to interact with the project file on disk. This +class is mostly useful for building scripts that use the Lektor API and +not so much in other situations as it's not directly playing a role in +the main build process. diff --git a/content/docs/api/project/discover/contents.lr b/content/docs/api/project/discover/contents.lr new file mode 100644 index 00000000..25d74836 --- /dev/null +++ b/content/docs/api/project/discover/contents.lr @@ -0,0 +1,22 @@ +title: discover +--- +type: method +--- +signature: base=None +--- +summary: Auto-discovers a project from a folder or the current working directory. +--- +body: + +This is the way Lektor by defaults discovers project files. It will start +out with the current path (of a given base path) and search upwards until it +finds a folder with a single project file in and then loads this. If none +of that leads to a file then `None` is returned. + +## Example + +```python +from lektor.project import Project + +project = Project.discover() +``` diff --git a/content/docs/api/project/from-file/contents.lr b/content/docs/api/project/from-file/contents.lr new file mode 100644 index 00000000..ac48a616 --- /dev/null +++ b/content/docs/api/project/from-file/contents.lr @@ -0,0 +1,21 @@ +title: from_file +--- +type: method +--- +signature: filename +--- +summary: Loads a project from a project file. +--- +body: + +This is the most common way to create a project insteance. It's a class +method that given the path to a project file will load the project. If +the file does not exist then `None` is returned. + +## Example + +```python +from lektor.project import Project + +project = Project.from_file('/path/to/the/project.lektorproject') +``` diff --git a/content/docs/api/project/make-env/contents.lr b/content/docs/api/project/make-env/contents.lr new file mode 100644 index 00000000..3eaec3b1 --- /dev/null +++ b/content/docs/api/project/make-env/contents.lr @@ -0,0 +1,25 @@ +title: make_env +--- +type: method +--- +signature: load_plugins=True +--- +summary: Creates an environment from a project. +--- +body: + +Once a project is loaded this method can be used to create a +[Environment :ref](../../environment/) object to work with. This is the +preferred way to construct such a thing. + +By default plugins will be loaded and initialized but this can be disabled +by passing `load_plugins=False`. + +## Example + +```python +from lektor.project import Project + +project = Project.discover() +env = project.make_env() +``` diff --git a/content/docs/api/templates/contents.lr b/content/docs/api/templates/contents.lr new file mode 100644 index 00000000..2be77253 --- /dev/null +++ b/content/docs/api/templates/contents.lr @@ -0,0 +1,9 @@ +title: Template API +--- +summary: Specific functionality added to the Jinja template environment by Lektor. +--- +body: + +Lektor extends the [Jinja2 :ext](http://jinja.pocoo.org/) templating language +with many useful helpers. We do not document Jinja 2 itself here, but all +the specific helpers are explained. diff --git a/content/docs/api/templates/filters/asseturl/contents.lr b/content/docs/api/templates/filters/asseturl/contents.lr new file mode 100644 index 00000000..fcc9b66a --- /dev/null +++ b/content/docs/api/templates/filters/asseturl/contents.lr @@ -0,0 +1,20 @@ +title: asseturl +--- +type: filter +--- +summary: Generates a relative URL from the current page to an asset. +--- +signature: alt=None +--- +body: + +This works similar to the [url](../url/) one but can only link to assets from +the `assets/` folder. However unlike `|url` it will append a dummy query +string with a hash of the source asset. This ensures that when the asset +changes it will be newly cached. + +## Example + +```html+jinja + +``` diff --git a/content/docs/api/templates/filters/contents.lr b/content/docs/api/templates/filters/contents.lr new file mode 100644 index 00000000..0dab42ee --- /dev/null +++ b/content/docs/api/templates/filters/contents.lr @@ -0,0 +1,8 @@ +title: Filters +--- +summary: Filters specific to Lektor that templates have access to. +--- +body: + +These is the list of custom filters added by Lektor in addition to the +[built in filters :ext](http://jinja.pocoo.org/docs/dev/templates/#builtin-filters). diff --git a/content/docs/api/templates/filters/dateformat/contents.lr b/content/docs/api/templates/filters/dateformat/contents.lr new file mode 100644 index 00000000..61b753d5 --- /dev/null +++ b/content/docs/api/templates/filters/dateformat/contents.lr @@ -0,0 +1,41 @@ +title: dateformat +--- +type: filter +--- +signature: date, format='medium', locale=None +--- +summary: Formats a date into a string +--- +body: + +To format a proper date (this is created by the [date +:ref](../../../../api/db/types/date/) type) into a string this filter can +be used. It will automatically format based on the language used by the +current context which is based on the locale of the current alt. + +A different language can be provided with the `locale` parameter. + +The following formats are locale specific: + +| Format | How it Looks +| -------- | ---------------- +| `full` | Saturday, December 5, 2015 +| `long` | December 5, 2015 +| `medium` | Dec 5, 2015 +| `short` | 12/5/15 + +In addition to that format codes from the CLDR project can be used. For +more information see [CLDR Date/Time Symbols +:ext](http://cldr.unicode.org/translation/date-time). + +## Examples + +```html+jinja +

Date: {{ this.pub_date|dateformat('full') }} +``` + +Or with custom CLDR format strings: + +```html+jinja +

Date: {{ this.pub_date|dateformat('EEE, MMM d, ''yy') }} +``` diff --git a/content/docs/api/templates/filters/latformat/contents.lr b/content/docs/api/templates/filters/latformat/contents.lr new file mode 100644 index 00000000..62b48900 --- /dev/null +++ b/content/docs/api/templates/filters/latformat/contents.lr @@ -0,0 +1,20 @@ +title: latformat +--- +type: filter +--- +signature: value, secs=True +--- +summary: Formats the latitude into a string +--- +body: + +This formats a latitude in float format into a human readable string in the +format degrees, minutes and seconds. See also [longformat :ref](../longformat/) +and [latlongformat :ref](../latlongformat/). + +## Examples + +```html+jinja +

Lat: {{ image.exif.latitude|latformat }} +

Long: {{ image.exif.longitude|longformat }} +``` diff --git a/content/docs/api/templates/filters/latlongformat/contents.lr b/content/docs/api/templates/filters/latlongformat/contents.lr new file mode 100644 index 00000000..36d11ab8 --- /dev/null +++ b/content/docs/api/templates/filters/latlongformat/contents.lr @@ -0,0 +1,19 @@ +title: latlongformat +--- +type: filter +--- +signature: value, secs=True +--- +summary: Formats the latitude and longitude into a string +--- +body: + +This formats a latitude and longitude tuple into a human readable string +in the format format degrees, minutes and seconds. See also [latformat +:ref](../latformat/) and [longformat :ref](../longformat/). + +## Examples + +```html+jinja +

Location: {{ image.exif.location|latlongformat }} +``` diff --git a/content/docs/api/templates/filters/longformat/contents.lr b/content/docs/api/templates/filters/longformat/contents.lr new file mode 100644 index 00000000..e3f596cd --- /dev/null +++ b/content/docs/api/templates/filters/longformat/contents.lr @@ -0,0 +1,20 @@ +title: longformat +--- +type: filter +--- +signature: value, secs=True +--- +summary: Formats the longitude into a string +--- +body: + +This formats a longitude in float format into a human readable string in the +format degrees, minutes and seconds. See also [latformat :ref](../latformat/) +and [latlongformat :ref](../latlongformat/). + +## Examples + +```html+jinja +

Lat: {{ image.exif.latitude|latformat }} +

Long: {{ image.exif.longitude|longformat }} +``` diff --git a/content/docs/api/templates/filters/tojson/contents.lr b/content/docs/api/templates/filters/tojson/contents.lr new file mode 100644 index 00000000..47636205 --- /dev/null +++ b/content/docs/api/templates/filters/tojson/contents.lr @@ -0,0 +1,27 @@ +title: tojson +--- +type: filter +--- +summary: Converts a data structure to JSON. +--- +body: + +This filter converts a value to JSON. It supports any of the types that are +supported by JSON directly and the return value will be in a format that +allows safe embedding in HTML. However if you want to use this value in an +attribute of an element it needs to use single quotes otherwise it will +generate a syntax error due to the embedded quotes. + +## Examples + +```html+jinja + +``` + +If used in attributes, single quotes are required: + +```html+jinja +Click here +``` diff --git a/content/docs/api/templates/filters/url/contents.lr b/content/docs/api/templates/filters/url/contents.lr new file mode 100644 index 00000000..917a8cec --- /dev/null +++ b/content/docs/api/templates/filters/url/contents.lr @@ -0,0 +1,52 @@ +title: url +--- +type: filter +--- +summary: Generates a relative URL from the current page to another. +--- +signature: alt=None +--- +body: + +This filter is the most useful filter for URL generation and it comes in two +flavors. It takes one optional argument which is the `alt` if it should +differ from the current one (see [Alternatives :ref](../../../../content/alts/)). +The filter can be applied to either a string or a [Record +:ref](../../../db/record/) object. + +Note that the URL filter operates on paths as they exist in your tree, not +as the final URLs. This is important when a page forcefully overrides the +URL path. In this case it could be that linking to `/project` for instance +would generate the URL `de/projekte/` if that's what's configured. + +If you want to override this behavior you can prefix the URL with `!` and +no resolving will take place. + +Note that the URL filter always generates a relative URL. So for instance +if you are currently at `/info/about/` and you want to link to `/projects/` +it will generate a link in the form of `../../projects/`. This makes it +possible to easily deploy the website to a folder outside of the root of +the website. + +## Examples + +```html+jinja +

+``` + +Same page to other alternative: + +```html+jinja +Русский +``` + +If you already know 100% where to link to, and you do not want any resolving +to take place, then you can prefix a path with `!`. For instance this always +links to the root of the website: + +```html+jinja +To Root +``` diff --git a/content/docs/api/templates/globals/bag/contents.lr b/content/docs/api/templates/globals/bag/contents.lr new file mode 100644 index 00000000..f5b5defe --- /dev/null +++ b/content/docs/api/templates/globals/bag/contents.lr @@ -0,0 +1,36 @@ +title: bag +--- +summary: Looks up a value from a databags. +--- +type: function +--- +signature: ... +--- +template_var: bag +--- +body: + +This handy function can look up data from [Databags :ref](../../../databags/). +The path to the values is a dotted path but alternatively multiple parameters +can be provided that will me concatenated into a dotted path. + +## Simple Example + +```html+jinja +{{ bag('site.title') }} +``` + +alternative method that does the same: + +```html+jinja +{{ bag('site', 'title') }} +``` + +## Example with Alternatives + +Because you can use arbitrary variables in it, this is a simple way to +load translations from a data bag: + +```html+jinja +{{ bag('i18n', alt, 'CLICK_HERE') }} +``` diff --git a/content/docs/api/templates/globals/contents.lr b/content/docs/api/templates/globals/contents.lr new file mode 100644 index 00000000..f259197c --- /dev/null +++ b/content/docs/api/templates/globals/contents.lr @@ -0,0 +1,9 @@ +title: Globals +--- +summary: Globals specific to Lektor that templates have access to. +--- +body: + +Certain variables and functions are always available in templates. Mostly this +gives you access to things like the [Pad :ref](../../db/pad/) but also useful +helper functions. diff --git a/content/docs/api/templates/globals/f/contents.lr b/content/docs/api/templates/globals/f/contents.lr new file mode 100644 index 00000000..58582c07 --- /dev/null +++ b/content/docs/api/templates/globals/f/contents.lr @@ -0,0 +1,12 @@ +title: F +--- +summary: Gives access to the field accessor. +--- +type: class +--- +template_var: F +--- +body: + +This gives access to the [F :ref](../../../db/f/) object for creating +expressions involving fields. For more information see the page there. diff --git a/content/docs/api/templates/globals/get-random-id/contents.lr b/content/docs/api/templates/globals/get-random-id/contents.lr new file mode 100644 index 00000000..ad647268 --- /dev/null +++ b/content/docs/api/templates/globals/get-random-id/contents.lr @@ -0,0 +1,22 @@ +title: get_random_id +--- +summary: Generates a new random ID. +--- +type: function +--- +template_var: get_random_id +--- +body: + +In some situations it can be very useful to generate a unique ID in templates. +This is particularly useful when creating templates for the [Flow +:ref](../../../../flow/) system and you need unique IDs for the DOM. + +## Example + +```html+jinja +{% set id = get_random_id() %} +
+ +
+``` diff --git a/content/docs/api/templates/globals/site/contents.lr b/content/docs/api/templates/globals/site/contents.lr new file mode 100644 index 00000000..924c6674 --- /dev/null +++ b/content/docs/api/templates/globals/site/contents.lr @@ -0,0 +1,12 @@ +title: site +--- +summary: Gives access to the current database pad. +--- +type: class +--- +template_var: site +--- +body: + +This gives access to the current database [Pad :ref](../../../db/pad/). For +more information see the page there. diff --git a/content/docs/api/utils/contents.lr b/content/docs/api/utils/contents.lr new file mode 100644 index 00000000..cb718b2d --- /dev/null +++ b/content/docs/api/utils/contents.lr @@ -0,0 +1,8 @@ +title: Utilities +--- +summary: Some useful utilities for plugins. +--- +body: + +For plugins there are some useful utilities provided with Lektor. This list +will only grow with time. diff --git a/content/docs/api/utils/get-structure-hash/contents.lr b/content/docs/api/utils/get-structure-hash/contents.lr new file mode 100644 index 00000000..acebfc30 --- /dev/null +++ b/content/docs/api/utils/get-structure-hash/contents.lr @@ -0,0 +1,25 @@ +title: get_structure_hash +--- +summary: Generates a hash from a Python structure. +--- +type: function +--- +module: lektor.utils +--- +signature: params +--- +body: + +Given a Python object (typically a dictionary or tuple) it generates a stable +MD5 hash from those recursively. This is useful for the `config_hash` +parameter with artifacts. + +## Example + +```pycon +>>> from lektor.utils import get_structure_hash +>>> get_structure_hash([1, 2, 3]) +'fbcf00cb8f6fc7631af7fbff70ca6f87' +>>> get_structure_hash([1, 2, 3, 4]) +'390e449c9748b72d6b9194a1c768d434' +``` diff --git a/content/docs/cli/build/contents.lr b/content/docs/cli/build/contents.lr new file mode 100644 index 00000000..4c203d0b --- /dev/null +++ b/content/docs/cli/build/contents.lr @@ -0,0 +1,45 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: build +--- +summary: Builds the entire project into the final... +--- +type: cmdlet +--- +body: + +`lektor build` + +Builds the entire project into the final artifacts. + +The default behavior is to build the project into the default build +output path which can be discovered with the `project-info` command +but an alternative output folder can be provided with the `--output-path` +option. + +The default behavior is to perform a build followed by a pruning step +which removes no longer referenced artifacts from the output folder. +Lektor will only build the files that require rebuilding if the output +folder is reused. + +To enforce a clean build you have to issue a `clean` command first. + +If the build fails the exit code will be `1` otherwise `0`. This can be +used by external scripts to only deploy on successful build for instance. + +## Options + +- `-O, --output-path PATH`: The output path. +- `--watch`: If this is enabled the build process goes into an automatic + loop where it watches the file system for changes and rebuilds. +- `--prune / --no-prune`: Controls if old artifacts should be pruned. + This is the default. +- `-v, --verbose`: Increases the verbosity of the logging. +- `--source-info-only`: Instead of building only updates the source infos. + The source info is used by the web admin panel to quickly find + information about the source files (for instance jump to files). +- `-f, --build-flag TEXT`: Defines an arbitrary build flag. These can be + used by plugins to customize the build process. More information can be + found in the documentation of affected plugins. +- `--profile`: Enable build profiler. +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/clean/contents.lr b/content/docs/cli/clean/contents.lr new file mode 100644 index 00000000..3bd6767a --- /dev/null +++ b/content/docs/cli/clean/contents.lr @@ -0,0 +1,23 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: clean +--- +summary: Cleans the entire build folder. +--- +type: cmdlet +--- +body: + +`lektor clean` + +Cleans the entire build folder. + +If not build folder is provided, the default build folder of the project +in the Lektor cache is used. + +## Options + +- `-O, --output-path PATH`: The output path. +- `-v, --verbose`: Increases the verbosity of the logging. +- `--yes`: Confirms the cleaning. +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/content-file-info/contents.lr b/content/docs/cli/content-file-info/contents.lr new file mode 100644 index 00000000..21816038 --- /dev/null +++ b/content/docs/cli/content-file-info/contents.lr @@ -0,0 +1,20 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: content-file-info +--- +summary: Provides information for a set of lektor files. +--- +type: cmdlet +--- +body: + +`lektor content-file-info [FILES]...` + +Given a list of files this returns the information for those files +in the context of a project. If the files are from different projects +an error is generated. + +## Options + +- `--json`: Prints out the data as json. +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/contents.lr b/content/docs/cli/contents.lr new file mode 100644 index 00000000..2f4e3c8c --- /dev/null +++ b/content/docs/cli/contents.lr @@ -0,0 +1,45 @@ +title: Command Line +--- +sort_key: 900 +--- +summary: Covers Lektor's command line interface. +--- +body: + +Lektor comes with a handy `lektor` command line executable you can use from +your terminal to manage all of Lektor in addition to the GUI. If you do +not have the Lektor command available because you only installed the GUI, +have a look at [Installation Documentation :ref](../installation/) to see +how to remedy this. + +All the commands documented here show up as subcommands to the global +`lektor` command. So to invoke `build` you would write `lektor build`. + +## Options + +There are some general options that can be set on the `lektor` command +to change the behavior. + +* `--project PATH`: explicitly provide the path to the project to work on. + If this is not provided then the project is searched upwards from the + current working directory until a folder is found with a single + `.lektorproject` file in it. +* `--version`: if this is passed it prints out the version of Lektor and + aborts further execution. +* `--help`: prints out help about the command line interface. + +## Environment Variables + +There are a few environment variables which control how Lektor executes: + +`LEKTOR_PROJECT` +> This can be used alternatively to `--project` to set the path to a +> project that Lektor should be using. If neither this variable nor +> `--project` is set, Lektor will look for a project upwards from the current +> working directory. + +`LEKTOR_OUTPUT_PATH` +> Overrides the default output path with a different path. By default the +> output path will be a path unique to the project but in a default cache +> folder under Lektor's control. `--output-path` on some commands overrides +> this value. diff --git a/content/docs/cli/deploy/contents.lr b/content/docs/cli/deploy/contents.lr new file mode 100644 index 00000000..82a5757e --- /dev/null +++ b/content/docs/cli/deploy/contents.lr @@ -0,0 +1,33 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: deploy +--- +summary: Deploy the website. +--- +type: cmdlet +--- +body: + +`lektor deploy [SERVER]` + +This command deploys the entire contents of the build folder +(`--output-path`) onto a configured remote server. The name of the +server must fit the name from a target in the project configuration. +If no server is supplied then the default server from the config is +used. + +The deployment credentials are typically contained in the project config +file but it's also possible to supply them here explicitly. In this +case the `--username` and `--password` parameters (as well as the +`LEKTOR_DEPLOY_USERNAME` and `LEKTOR_DEPLOY_PASSWORD` environment +variables) can override what's in the URL. + +For more information see the deployment chapter in the documentation. + +## Options + +- `-O, --output-path PATH`: The output path. +- `--username TEXT`: An optional username to override the URL. +- `--password TEXT`: An optional password to override the URL or the + default prompt. +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/dev/contents.lr b/content/docs/cli/dev/contents.lr new file mode 100644 index 00000000..c5c7e917 --- /dev/null +++ b/content/docs/cli/dev/contents.lr @@ -0,0 +1,22 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: dev +--- +summary: Development commands. +--- +type: cmdlet +--- +body: + +`lektor dev` + +Development commands for Lektor. + +This provides various development support commands for Lektor. This is +primarily useful for Lektor plugin development but also if you want to +extend Lektor itself. Additional functionality can be unlocked by +exporting the `LEKTOR_DEV=1` environment variable. + +## Options + +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/dev/new-plugin/contents.lr b/content/docs/cli/dev/new-plugin/contents.lr new file mode 100644 index 00000000..27dd9327 --- /dev/null +++ b/content/docs/cli/dev/new-plugin/contents.lr @@ -0,0 +1,24 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: new-plugin +--- +summary: Creates a new plugin +--- +type: cmdlet +--- +body: + +`lektor dev new-plugin [PLUGIN_NAME]` + +This command creates a new plugin. + +This will present you with a very short wizard that guides you through +creation of a new plugin. At the end of it, it will create a plugin +in the packages folder of the current project or the path you defined. + +This is the fastest way to creating a new plugin. + +## Options + +- `--path PATH`: The destination path +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/dev/publish-plugin/contents.lr b/content/docs/cli/dev/publish-plugin/contents.lr new file mode 100644 index 00000000..0d5559ad --- /dev/null +++ b/content/docs/cli/dev/publish-plugin/contents.lr @@ -0,0 +1,20 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: publish-plugin +--- +summary: Publish a plugin to PyPI. +--- +type: cmdlet +--- +body: + +`lektor dev publish-plugin` + +Publishes the current version of the plugin in the current folder. + +This generally requires that your setup.py has at least the bare minimum +configuration for valid publishing to PyPI. + +## Options + +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/dev/shell/contents.lr b/content/docs/cli/dev/shell/contents.lr new file mode 100644 index 00000000..79e582e2 --- /dev/null +++ b/content/docs/cli/dev/shell/contents.lr @@ -0,0 +1,27 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: shell +--- +summary: Starts a python shell. +--- +type: cmdlet +--- +body: + +`lektor dev shell` + +Starts a Python shell in the context of a Lektor project. + +This is particularly useful for debugging plugins and to explore the +API. To quit the shell just use `quit()`. Within the shell various +utilities are available right from the get-go for you. + + +- `project`: the loaded project as object. +- `env`: an environment for the loaded project. +- `pad`: a database pad initialized for the project and environment + that is ready to use. + +## Options + +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/plugins/add/contents.lr b/content/docs/cli/plugins/add/contents.lr new file mode 100644 index 00000000..c3eba5c5 --- /dev/null +++ b/content/docs/cli/plugins/add/contents.lr @@ -0,0 +1,23 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: add +--- +summary: Adds a new plugin to the project. +--- +type: cmdlet +--- +body: + +`lektor plugins add NAME` + +This command can add a new plugion to the project. If just given +the name of the plugin the latest version of that plugin is added to +the project. + +The argument is either the name of the plugin or the name of the plugin +suffixed with `@version` with the version. For instance to install +the version 0.1 of the plugin demo you would do `demo@0.1`. + +## Options + +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/plugins/contents.lr b/content/docs/cli/plugins/contents.lr new file mode 100644 index 00000000..48724053 --- /dev/null +++ b/content/docs/cli/plugins/contents.lr @@ -0,0 +1,18 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: plugins +--- +summary: Manages plugins. +--- +type: cmdlet +--- +body: + +`lektor plugins` + +This command group provides various helpers to manages plugins +in a Lektor project. + +## Options + +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/plugins/flush-cache/contents.lr b/content/docs/cli/plugins/flush-cache/contents.lr new file mode 100644 index 00000000..3545df8a --- /dev/null +++ b/content/docs/cli/plugins/flush-cache/contents.lr @@ -0,0 +1,19 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: flush-cache +--- +summary: Flushes the plugin installation cache. +--- +type: cmdlet +--- +body: + +`lektor plugins flush-cache` + +This uninstalls all plugins in the cache. On next usage the plugins +will be reinstalled automatically. This is mostly just useful during +plugin development when the cache got corrupted. + +## Options + +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/plugins/list/contents.lr b/content/docs/cli/plugins/list/contents.lr new file mode 100644 index 00000000..1fc5055d --- /dev/null +++ b/content/docs/cli/plugins/list/contents.lr @@ -0,0 +1,23 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: list +--- +summary: List all plugins. +--- +type: cmdlet +--- +body: + +`lektor plugins list` + +This returns a list of all currently actively installed plugins +in the project. By default it only prints out the plugin IDs and +version numbers but the entire information can be returned by +increasing verbosity with `-v`. Additionally JSON output can be +requested with `--json`. + +## Options + +- `--json`: Prints out the data as json. +- `-v, --verbose`: Increases the verbosity of the output. +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/plugins/reinstall/contents.lr b/content/docs/cli/plugins/reinstall/contents.lr new file mode 100644 index 00000000..7ea16038 --- /dev/null +++ b/content/docs/cli/plugins/reinstall/contents.lr @@ -0,0 +1,21 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: reinstall +--- +summary: Reginstall all plugins. +--- +type: cmdlet +--- +body: + +`lektor plugins reinstall` + +Forces a re-installation of all plugins. This will download the +requested versions of the plugins and install them into the plugin +cache folder. Alternatively one can just use `flush-cache` to +flush the cache and on next build Lektor will automatically download +the plugins again. + +## Options + +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/plugins/remove/contents.lr b/content/docs/cli/plugins/remove/contents.lr new file mode 100644 index 00000000..4792eea8 --- /dev/null +++ b/content/docs/cli/plugins/remove/contents.lr @@ -0,0 +1,18 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: remove +--- +summary: Removes a plugin from the project. +--- +type: cmdlet +--- +body: + +`lektor plugins remove NAME` + +This command can remove a plugion to the project again. The name +of the plugin is the only argument to the function. + +## Options + +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/project-info/contents.lr b/content/docs/cli/project-info/contents.lr new file mode 100644 index 00000000..3ac3a0c5 --- /dev/null +++ b/content/docs/cli/project-info/contents.lr @@ -0,0 +1,25 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: project-info +--- +summary: Shows the info about a project. +--- +type: cmdlet +--- +body: + +`lektor project-info` + +Prints out information about the project. This is particular +useful for script usage or for discovering information about a +Lektor project that is not immediately obvious (like the paths +to the default output folder). + +## Options + +- `--json`: Prints out the data as json. +- `--name`: Print the project name +- `--project-file`: Print the path to the project file. +- `--tree`: Print the path to the tree. +- `--output-path`: Print the path to the default output path. +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/quickstart/contents.lr b/content/docs/cli/quickstart/contents.lr new file mode 100644 index 00000000..8eef64a4 --- /dev/null +++ b/content/docs/cli/quickstart/contents.lr @@ -0,0 +1,19 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: quickstart +--- +summary: Starts a new empty project. +--- +type: cmdlet +--- +body: + +`lektor quickstart` + +Starts a new empty project with a minimum boilerplate. + +## Options + +- `--name TEXT`: The name of the project. +- `--path PATH`: Output directory +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/cli/server/contents.lr b/content/docs/cli/server/contents.lr new file mode 100644 index 00000000..6876a9b3 --- /dev/null +++ b/content/docs/cli/server/contents.lr @@ -0,0 +1,32 @@ +comment: This file is auto generated by dump-cli-help.py +--- +title: server +--- +summary: Launch a local server. +--- +type: cmdlet +--- +body: + +`lektor server` + +The server command will launch a local server for development. + +Lektor's developemnt server will automatically build all files into +pages similar to how the build command with the `--watch` switch +works, but also at the same time serve up the website on a local +HTTP server. + +## Options + +- `-h, --host TEXT`: The network interface to bind to. The default is the + loopback device, but by setting it to 0.0.0.0 it becomes available on + all network interfaces. +- `-p, --port INTEGER`: The port to bind to. +- `-O, --output-path PATH`: The dev server will build into the same folder + as the build command by default. +- `-v, --verbose`: Increases the verbosity of the logging. +- `-f, --build-flag TEXT`: Defines an arbitrary build flag. These can be + used by plugins to customize the build process. More information can be + found in the documentation of affected plugins. +- `--help`: print this help page. \ No newline at end of file diff --git a/content/docs/content/alts/contents.lr b/content/docs/content/alts/contents.lr new file mode 100644 index 00000000..fb1944f1 --- /dev/null +++ b/content/docs/content/alts/contents.lr @@ -0,0 +1,68 @@ +title: Alternatives +--- +summary: Alternatives allow you to localize or internationalize a site. +--- +body: + +One particular useful feature of Lektor is the ability to define alternatives +to a content file. This allows you to translate your website into many +different languages or to customize content specifically for some languages +or regions. + +## Enabling Alternatives + +To enable alternatives you need to extend your [Project File +:ref](../../project/). For each alternative a new +section has to be added. It's important that one of the alternatives is +marked as "primary" which informs the system which of the alternatives is +the reference. + +```ini +[alternatives.en] +name = English +primary = yes +locale = en_US + +[alternatives.fr] +name = French +url_prefix = /fr/ +locale = fr +``` + +The `locale` key is used to define the locale that Lektor assumes for the +alternative. This is for instance used for date formatting. + +## Content Files of Alternatives + +Once alternatives are enabled your website will be built multiple times. Once +for each alternative. By default nothing will change as no content files +exist for the non primary alternatives. In the example of above if you +navigate to `/fr/` you will see the same index page as if you would have +navigated to `/`. + +However you can now start translating your content. Each alternative has +a content file named `contents+ALT.lr` where `ALT` is the short code of your +alternative (for instance `fr` for French). Once a file for an alternative +exists it's loaded instead of the default `contents.lr` file. + +## What's Loaded + +To make this a bit more concrete: let's go with the example we configured +above. There are two alternatives: English (`en`) and French (`fr`) and +English is configured as the primary. + +In that case depending on which files exist and which alternative is +targeted, different files will be used. This table visualizes this: + +| contents.lr | contents+fr.lr | contents+en.lr | Target | File Used +| ----------- | -------------- | -------------- | ------ | ------------ +| ✓ | | | en | contents.lr +| ✓ | | | fr | contents.lr +| ✓ | ✓ | | en | contents.lr +| ✓ | ✓ | | fr | contents+fr.lr +| | ✓ | ✓ | en | contents+en.lr +| | ✓ | ✓ | fr | contents+fr.lr +| | ✓ | | en | *missing* +| | ✓ | | fr | contents+fr.lr +| | | ✓ | en | contents+en.lr +| | | ✓ | fr | *missing* diff --git a/content/docs/content/attachments/contents.lr b/content/docs/content/attachments/contents.lr new file mode 100644 index 00000000..6743fec7 --- /dev/null +++ b/content/docs/content/attachments/contents.lr @@ -0,0 +1,49 @@ +title: Attachments +--- +summary: A quick introduction in how attachments are handled in Lektor. +--- +body: + +Each page in Lektor can have attachments appended. These files are stored +directly in the page folder and become available publicly. + +## Attachment Types + +For the most part Lektor does care much about what types your attachments +are but it will specially handle some. In particular image formats supported +by browsers have special support for automatic thumbnailing and accessing +basic image data. + +The following file formats are specially handled: + +| Type | Extensions +| -------- | ---------------------------- +| Image | .jpg .jpeg .gif .png +| Video | .avi, .mpg, .mpeg, .wmv, .ogv +| Audio | .mp3, .wav, .ogg + +## Metadata + +Attachments can also be given metadata. For this you need to create a lektor +content file with the same name as the attachment but with `.lr` added as +extra extension: + +| File | Metadata file +| ----------- | --------------- +| sunset.jpeg | sunset.jpeg.lr +| code.py | code.py.lr + +Attachments can be [given a default model :ref](../../models/#attachments) or +a model can be explicitly given in the content file with the `_model` field. + +Here a basic example: + +``` +_model: image +---- +description: A beautiful sunset. +---- +photographer: Mr. Peter John Doe +---- +copyright: 2015 by Mr. Peter John Doe +``` diff --git a/content/docs/content/contents.lr b/content/docs/content/contents.lr new file mode 100644 index 00000000..e614f782 --- /dev/null +++ b/content/docs/content/contents.lr @@ -0,0 +1,90 @@ +title: Content +--- +summary: Learn how the content is stored, built and how you can edit it. +--- +sort_key: 80 +--- +body: + +Lektor builds a website by taking all the files in the `content/` folder, +processing them according to the rules of your [Models :ref](../models/) +and rendering them by using templates. Don't worry. It's easier than it +sounds. + +## One Folder — One Page + +Each page (or each URL) corresponds to a folder below the `content/` folder. +There can be as many folders as you want and they can be arbitrarily nested. +Within each folder there needs to be at least one file, the content file: +`contents.lr`. Into that file, your data goes. + +All the other files in a folder are considered attachments of the page. + +Here is an example structure from a website: + +``` +content/ + contents.lr + portfolio/ + contents.lr + project-a/ + contents.lr + thumbnail.jpg + project-b/ + contents.lr + thumbnail.jpg + about/ + contents.lr +``` + +## One Page — One URL + +Out of each `contents.lr` file, Lektor builds exactly one final page on +exactly one URL. So if you have `content/portfolio/project-a/contents.lr` +the rendered end result will be at `/portfolio/project-a/`. + +## Page, Model and Template + +Each page is associated with a model and a template. Each page needs to have +a model that defines with fields exist. The template by default matches the +model name but it can be overridden on a per-page basis. + +So how is the model selected? Either expicitly in the `contents.lr` file +with the `_model` key or by configuration and convention. Lektor will +select the default model based on trying things in this order: + +1. configured model in contents file +2. model configured from a parent model for all children +3. model matching the page ID +4. the model named `page` + +The template is always the name of the model with `.html` as extension. If +can be overridden in the content file with the `_template` field. + +## Content File Format + +So now it's time to talk about the content file. The content file is just a +text file with the `.lr` extension. It can be edited with any text editor +that supports UTF-8 as character encoding. This file consists of multiple +data fields according to the model. The format is very simple: + +``` +_model: page +---- +title: The Page Title +---- +body: + +The page body goes here +``` + +Fields are separated by three dashes `---` and follow the format `key: value`. +For values with multiple lines it's recommended to insert two newlines after +the key. To format of each field is specific to how the model is configured. + +Some fields are plain text, others can be markdown syntax and more. These +fields become available for rendering in the template automatically. + +**Tip:** If you want to use `---` itself in the document text, just add another +dash. This means `----` will render as `---` and `-----` will render as +`----` etc. diff --git a/content/docs/content/databags/contents.lr b/content/docs/content/databags/contents.lr new file mode 100644 index 00000000..af5e880b --- /dev/null +++ b/content/docs/content/databags/contents.lr @@ -0,0 +1,63 @@ +title: Data Bags +--- +summary: A summary about using the data bag system to store useful data. +--- +body: + +The [Data Bags :ref](../../api/databags/) are a quick way to store structured +information that templates can access. It's just a convenient way to access +data from INI or JSON files. This is useful for instance to build a navigation +menu or similar things. + +## Creating Bags + +To create a data bag just place a `.ini` or `.json` file in the `databags/` +folder. The files there are accessible by their name sans the file extension. +All ordering in the source files is retained. So for instance this could +be the `main-nav.ini` data file for a basic navigation: + +```ini +/downloads = Download +/docs = Documentation +/blog = Blog +``` + +And the template could access it like this: + +```html+jinja + +``` + +## Nested Access + +Data bags can be structured in more complex ways. For INI files sections +can be used, for JSON files entire object trees can be stored. The +[bag :ref](../../api/templates/globals/bag/) function can thus accept +arbitrary dotted path (or multiple args to `bag`) to navigate to specific items +within the bag. For instance you could use this to look up values that might +change depending on the alternative of a page for instance. + +In this case the system is used to translate buttons. Take `buttons.ini` +as an example: + +```ini +[en] +download = Download + +[de] +download = Herunterladen + +[ru] +download = Скачать +``` + +And in a template it could be used like this: + +```html+jinja +

{{ bag('buttons', this.alt, 'download') }}

+``` diff --git a/content/docs/content/flow/contents.lr b/content/docs/content/flow/contents.lr new file mode 100644 index 00000000..6f17e08c --- /dev/null +++ b/content/docs/content/flow/contents.lr @@ -0,0 +1,107 @@ +title: Flow +--- +summary: Learn about how to use the flow system to build more flexible pages. +--- +body: + +Flow is a system in Lektor that allows you to have higher flexibility when +creating pages. The [Flow Type :ref](../../api/db/types/flow/) field type +that allows you to store multiple different formats of data within the same +field each with it's own template. + +This allows you to build complex pages made from individual components. + +## How does Flow Work? + +When you add a `flow` field to one of your models you essentially gain the +ability to attach as many blocks as you want into this field. Each block +corresponds to a specific [Flow Block Models :ref](../../models/flow/) and +can render a separate template. + +This is a very powerful feature as it allows you to be very flexible in the +designs you want to achieve for your website. As an example, the +[Front Page](../../../) of this website uses many different blocks to +implement the slideshow, feature list and more. + + + +## Flow Block Models + +To use the `flow` type you need to define some blocks. This works pretty +much the same as with creating [Data Models :ref](../../models/) except they +go into the `flowblocks` folder and have slightly different parameters. + +For more information about this see [Flow Block Models +:ref](../../models/flow/). + +## Block Templating + +Flow blocks appear as elements of the flow type. When it's rendered in a +template it will render each block separately one after another and connect +them with newlines. These blocks can also manually be accessed however. +Each flow block renders a template named after itself from the `blocks` +folder in the template directory. So if you have a flow block model +named `text` it will render from `templates/blocks/text.html`. + +Within a template a flow block has access to all the variables that you would +expect. `this` points to the current block and `record` points to the record +the block is contained in. + +This is a simple example of a typical template of a flow block: + +```html+jinja +
+ {{ this.text }} +
+``` + +Some more involved templates might also refer back to the page to do something +with it. Here for instance is a simple flow block for including a thumbnail +of an attached image: + +```html+jinja +{% set img = record.attachments.images.get(this.image) %} +{% if img %} +
+ +
+{% endif %} +``` + +This also has a safeguard that if the image does not exist nothing is rendered +instead. This is a good idea because someone could delete the attachments but +not update the reference in the block. + +## Flow Templating + +The default behavior of rendering an entire flow is to render the templates +of the individual blocks one after another. Sometimes that is not flexible +enough. Because of that you can access the blocks individually. This is +particularly useful for more complex situations where you either deal with +nested flows or where you want to wrap the individual blocks somehow. + +For instance in this case the blocks are wrapped before rendering: + +```html+jinja +{% for blk in this.demo_flow.blocks %} +
+ {{ blk }} +
+{% endfor %} +``` + +The `_flowblock` field is always available on a flow block and is the name +of the data model that defines it. This is a good way to also specifically +customize individual blocks in a flow without having to invoke their +templates. You can for instance completely manually render blocks without +using their templates: + +```html+jinja +{% for blk in this.demo_flow.blocks %} + {% if blk._flowblock == 'text' %} +

{{ blk.text }} + {% elif blk._flowblock == 'image' %} + {{ blk.alt }} + {% endif %} +{% endfor %} +``` diff --git a/content/docs/content/flow/flow-admin.png b/content/docs/content/flow/flow-admin.png new file mode 100644 index 00000000..adcf5da1 Binary files /dev/null and b/content/docs/content/flow/flow-admin.png differ diff --git a/content/docs/content/urls/contents.lr b/content/docs/content/urls/contents.lr new file mode 100644 index 00000000..38149299 --- /dev/null +++ b/content/docs/content/urls/contents.lr @@ -0,0 +1,103 @@ +title: URLs and Slugs +--- +summary: Explains how URLs and slugs work in Lektor. +--- +body: + +Lektor's URLs in general mirror what happens on the file system as much as +possible. There are however various cases in which this behavior can be +changed. + +## A URL Made of Slugs + +A URL in Lektor is built of slugs. What are slugs? Slugs are parts of the +URL which can be customized. They are roughly speaking something like a +file name. For instance `foo-bar` can be a slug in `/foo-bar/`. The default +slug of a page is the ID of the page. So if you have a page called +`/foo/bar/contents.lr` then the default slug is `bar`. As you can see the +full URL is comprised of it's own slug concatenated with all the slugs of +all parents. + +Can a slug contain slashes? Yes indeed it can. A slug is free to contain +any slashes if it wants and they will be handled just as you expect. So it's +perfectly valid for a page to have `2015/5/demo` as slug. What's not possible +is for a page to pretent that it belongs to a different parent. The parent +paths are always added to it. So once the page is below `/foo` the URL path +will always begin with the URL path of the page `foo`. + +## Slug Customization + +As mentioned slugs can be customized. There are three systems that control +what the slug looks like: + +### Folder Name + +The name of the folder that contains the `contents.lr` file is the ID of the +page and thus the default slug (unless changed by the model — more about that +later). This means that you could for instance just rename the folder to +get a different slug. This is the recommended way for the majority of pages +to adjust the slug. + +### The `_slug` System Field + +The second option is to use the `_slug` system field. This field is available +for all models automatically and overrides the slug explicitly. This is +particularly useful to force a slug that could not be represented on the file +system (for instance because it should contain a slash) or because you want +to change the slug for a different [Alternative :ref](../alternatives/). As +an example a page translated to German might want to translate the slug as well. + +### Implied Slug Configuration + +The last part is a system that controls the implied slugs. In particular it +means that the model of the parent page can override the default slug for all +of the children below it. This for instance can be used to automatically add +the date of a blog post into the slug. For more information about this +feature see [Children & Pagination :ref](../../models/children/). + +## Extensions and File Types + +The default behavior for a page is to build into a hidden `index.html` file. +This means that if you have a page called `foo/bar/contents.lr` it will +build into a file named `foo/bar/index.html`. Web servers typically look for +an `index.html` file in a folder which is why you can just access `/foo/bar/` +and the page will render. + +If however the last path component contains a period (`.`) then the last path +component is assumed to be a filename directly. This means that if you set +the slug of a page to `404.html` for instance, the page will indeed render +into `404.html` and not `404/index.html`. + +However you need to be careful with this as web servers pick the “mimetype” +of a file based on the extension. So if you name a file `foo.html` it will +behave very differently compared to a file named `foo.txt`. So do not name +your files in ways that would be incompatible with what the web server +renders. This however also allows you to generate files that are not +HTML. For instance you could render into a `.xml` file if that is what +you want. + +This feature is called “Dotted Slugs” and there are some specifics with +regards to how these are handled. + +## Content Below Dotted Slugs + +One specific behavior of a path component that contains a dotted slug is that +content below that path need to move to a different location. Imagine for +instance you named the page `404.html` but that page has an attachment. As +`404.html` is now a file it's impossible for a folder with the same name to +exist. This means that the attachments have to be stored elsewhere. The +convention for this in Lektor is to prefix the path with an underscore. So +if `404.html` has an attachment named `foo.jpeg` it will move into +`_404.html/foo.jpeg`. + +## External and Absolute URLs + +Lektor will generally prefer relative URLs when it has a choice to use them. +This makes it possible to easily host a website below a certain folder +without having to do anything special to make this work. However there are +some features which will require the use of an absolute URL. For instance +sitemaps or Atom feeds do not work with realtive URLs. In this case the +absolute URL path has to be configured. + +To see how this can be configured see [Project Configuration +:ref](../../project/file/#[project]). diff --git a/content/docs/contents.lr b/content/docs/contents.lr new file mode 100644 index 00000000..dd74bb48 --- /dev/null +++ b/content/docs/contents.lr @@ -0,0 +1,17 @@ +_model: doc-page +--- +title: Documentation +--- +allow_comments: no +--- +body: + +Welcome to the Lektor documentation center. Here you can find everything +about how to use Lektor from both a developer's as well as an end user's +point of view. New to Lektor? Then read about [what makes Lektor +special :ref](what/). + +The documentation is work in progress and there will be areas that are +lacking. If you have any feedback or you want to help out with the +documentation project you can head over to the +[GitHub Repository :ext](https://github.com/lektor/lektor-website) of this website. diff --git a/content/docs/deployment/contents.lr b/content/docs/deployment/contents.lr new file mode 100644 index 00000000..d42f5e70 --- /dev/null +++ b/content/docs/deployment/contents.lr @@ -0,0 +1,115 @@ +title: Deployment +--- +sort_key: 110 +--- +summary: All there is to know about deploying and publishing changes. +--- +body: + +A website is only a website if other people can look at it. While you are +developing locally that's not really all that helpful. So how do you get +your changes up to your favorite web host? This is where [lektor deploy +:ref](../cli/deploy/) comes in. + +## Deploying in Two Steps: + +Deploying a website in Lektor is a two step process: + +1. build +2. deploy + +Keep this in mind. A deploy will not implicitly build! This means that if +you deploy without building first you might send up a completely wrong +version! Also more importantly: *never deploy unless the build finished +successfully*. + +## The Build Pipeline + +So let's cover the building first. When you use the Lektor server locally, +Lektor constantly builds our your website into static HTML files behind the +scenes into the default build folder. This folder is in an operating system +specific location. If you want to know where that folder is, you can use this +command: + +``` +$ lektor project-info --output-path +``` + +Additionally you can manually provide a different path if you kick off a +manual build: + +``` +$ lektor build --output-path my-folder +``` + +Generally we strongly recommend to use the default build folder when deploying +from your own machine because it will be faster since the build can reuse +what the development server already did. If you want to deploy from a build +server it might make more sense to provide absolute paths. + +## Lektor Assisted Deployments + +Currently Lektor can deploy via `rsync` and `ftp` automatically. To enable +this functionality you need to configure this in the config file. For each +potential deployment target add a `[servers.NAME]` section. The supported +keys are `name` for an optional human readable name of the server, `enabled` to +enable or disable it (defaults to `true`) and `target` which is the URL to +publish to. Additionally one of the servers can have `default` set to `yes` +to set it as default. Here an example: + +```ini +[servers.production] +name = Production +enabled = yes +default = yes +target = rsync://server/path/to/folder +``` + +To trigger a deploy you can use the [deploy :ref](../cli/deploy/) command: + +``` +$ lektor deploy production +``` + +Or because it's the default server, you can just do this: + +``` +$ lektor deploy +``` + +This deploys the latest state of what was built from the default build +folder. It does not build itself! So to do both in one go, do something like +this: + +``` +$ lektor build && lektor deploy +``` + +If you want to provide a different build folder, provide it explicitly with +`--output-path` to both `build` and `deploy`. This can also be set with +the `LEKTOR_OUTPUT_PATH` environment variable. + +**Note on credentials**: For FTP and other systems it is possible to embed +the credentials directly in the URL. If you do this, please ensure that +you keep your project file secure as loss of the project file can mean that +people get access to your server. Alternatively you can also provide username +and password through the command line of environment variables. + +The following targets are supported for the `target` folder: + +* [rsync](rsync/) +* [FTP](ftp/) +* [GitHub Pages](ghpages/) + +## Manual Deployments + +If you want to manually deploy something through the favorite tool of yours +you can do that easily as well. For instance if you want to deploy to S3 +with [s3cmd](http://s3tools.org/s3cmd-sync) you could do this: + +``` +$ lektor build && s3cmd sync "$(lektor project-info --output-path)" "s3://my-bucket/some/path" +``` + +Assisted deployments are also supported directly from the admin UI. There is a +publish button that can be used to send the changes up. diff --git a/content/docs/deployment/ftp/contents.lr b/content/docs/deployment/ftp/contents.lr new file mode 100644 index 00000000..3eadd130 --- /dev/null +++ b/content/docs/deployment/ftp/contents.lr @@ -0,0 +1,61 @@ +title: FTP +--- +summary: Automatic deployments via FTP +--- +body: + +* `ftp://username:password@server:port/path?passive=yes|no` +* `sftp://username:password@server:port/path?passive=yes|no` + +FTP is a very old but well supported protocol. Lektor can publish through it +but to speed up the operation it has to make certain concessions. In +particular it needs to maintain a “listing file” to remember the state of +the files as FTP does not support any form of file comparisions. This means +that you cannot use Lektor in addition to another system to publish files +to your FTP server. It's an either/or thing. + +If you have alternatives to using FTP it's recommended to use those. FTP is +a very simple transport format, very slow, underspecified, largely insecure +and not very portable. + +The system supports FTP (`ftp://`) and FTP over TLS (`sftp://`). Passive mode +can be enabled/disabled with the optional `?passive` parameter. It defaults +to true. + +## Example + +```ini +[servers.production] +target = sftp://myuser:mypassword@ftp.example.com/var/www/example +``` + +## Note on Credentials + +FTP is considered a largely insecure protocol for Lektor. As such if you +want to use it you should keep your project file save as credentials will +be most likely embedded there. Alternatively you can set the credentails +via the command line with the `--username` and `--password` option (or via the +environment variables `LEKTOR_DEPLOY_USERNAME` and `LEKTOR_DEPLOY_PASSWORD`) +though this will mean that most likely deploys via the admin interface +will fail as the values will not be available there. + +## Implementation + +If you want to know how the FTP sync process works, here is how the system +figures out what to sync and what not. When it synchronizes it maintains +a “listing file” in `.lektor/listing`. This file contains the checksums of +all uploaded artifacts. If that file desynchronizes with the actual files +that were uploaded it can be deleted and Lektor will sync up everything. +The file is modified through append operations and at the end of the sync +duplicate entries in that file are resolved. + +## Using Other Tools + +The FTP support in Lektor is quite rudimentary and in some situations you +might want to use a different tool like filezilla to do your synchronziations +instead. In that case you just need to point your FTP client to the build +folder. To find out where this is, just run this command: + +``` +$ lektor project-info --output-path +``` diff --git a/content/docs/deployment/ghpages/contents.lr b/content/docs/deployment/ghpages/contents.lr new file mode 100644 index 00000000..2592d066 --- /dev/null +++ b/content/docs/deployment/ghpages/contents.lr @@ -0,0 +1,61 @@ +title: GitHub Pages +--- +summary: Deploys to github pages. +--- +body: + +* `ghpages://username/repository` +* `ghpages+https://username/repository` + +A popular way to host websites for Open Source projects is the GitHub pages. +It's a free services provided by [GitHub :ext](http://github.com/) which allows +to host completely static websites through GitHub. + +The way this is implemented in Lektor currently is that Lektor will force-push +a website into a repository of choice. There are two ways to push it up: +`ghpages` (which uses SSH) or `ghpages+https` (which uses HTTPS). The latter +can also accept `username:password@` in the URL to hold the credentials in +addition to accepting username and password from the command line. + +Example: + +```ini +[servers.production] +target = ghpages://your-user/your-repository +``` + +## Behavior + +The way this deployment support works is that it commits a new commit into a +temporary location and pushes it into the `gh-pages` or `master` branch +depending on the name of the repository. If you push to `username.github.io` +then it commits to `master`, otherwise to `gh-pages`. This is consistent +with behavior for GitHub Pages. + +## CNAME Support + +If you want to use a [CNAME :ext](https://en.wikipedia.org/wiki/CNAME) with +GitHub pages and Lektor you can provide the intended CNAME with the `?cname` +parameter: + +```ini +[servers.production] +target = ghpages://your-user/your-repository?cname=www.example.com +``` + +For more information about how CNAMEs work with GitHub you can read about +the feature in the GitHub help center: +[Adding a CNAME file to your repository +:ext](https://help.github.com/articles/adding-a-cname-file-to-your-repository/). + +## 404 Pages + +Per convenention the file named `404.html` is used as placeholder if a page +cannot be found. You can create such a page by creating a `404.html/contents.lr` +file. + +## Automatic Deployments + +If you want to use ghpages it's desirable to have this build automatically. +This is easy to accomplish with the help of Travis-CI. For more information +see [Deploying with Travis-CI :ref](../travisci/). diff --git a/content/docs/deployment/rsync/contents.lr b/content/docs/deployment/rsync/contents.lr new file mode 100644 index 00000000..e52414be --- /dev/null +++ b/content/docs/deployment/rsync/contents.lr @@ -0,0 +1,27 @@ +title: rsync +--- +summary: Automatic deployments with SSH and rsync. +--- +body: + +`rsync://username@server/path/to/folder` + +This deploys via SSH and rsync to a remote server. This is the recommended +way to deploy if the system supports it as it is a very fast and realiable +way. It uses the system's SSH config so for authentication just configure +SSH as you would normally do. The `username` part is optional and defaults +to the current user that is signed into the machine or whatever is picked up +as default from the `.ssh/config`. + +## Example + +```ini +[servers.production] +target = rsync://deploy@example.com/var/www/example.com +``` + +## Note on Credentials + +The `rsync` deploy method supports both username and password parameter +though it's recommended to use `.ssh/config` and an SSH agent to secure +the deployment. This is outside of the context of this documentation. diff --git a/content/docs/deployment/travisci/contents.lr b/content/docs/deployment/travisci/contents.lr new file mode 100644 index 00000000..7cdadb32 --- /dev/null +++ b/content/docs/deployment/travisci/contents.lr @@ -0,0 +1,120 @@ +title: Travis-CI +--- +summary: Automated deployments via Travis-CI. +--- +body: + +For certain websites it can be interesting to use +[Travis-CI](https://travis-ci.org/) to automatically deploy the latest version +of a website from a github repository. This is particularly useful when +coupled with the [GitHub Pages :ref](../ghpages/) deployment method which is +what we're going to cover in this guide. But you can easily adjust it to +any other method. + +This assumes you already signed up for Travis-CI. If you have not, just +head to [travis-ci.org :ext](https://travis-ci.org/) and sign up with your +GitHub account. + +This guide is also available as a 7 minute screencast: + + + +## Travis Config + +Once you have signed up for Travis-CI you need to add a `.travis.yml` config +file into your repository. You can copy paste this over: + +```yaml +language: python +python: 2.7 +install: "pip install Lektor" +script: "lektor build && lektor deploy ghpages" +``` + +Because Travis already comes with all dependencies we need other than +Lektor itself we just need to pip install Lektor and we're ready to go. For +the build step we just invoke `lektor build` and on success we invoke +`lektor deploy ghpages` to ship it to the ghpages server. We still need +to configure that. + +## Project Server Config + +For the above example the best way to configure the server for the deployment +in the project file would be to use `ghpages+https` like this: + +```ini +[servers.ghpages] +target = ghpages+https://username/repository +``` + +You need to add this to your `.lektorproject` file. + +Whenever Travis builds it will automatically throw the end result into the +`gh-pages` branch and the website updates. We do however still need to +configure the access credentials. We will get to that. + +## Enabling Travis + +So now that we have all that configured we need to tellt travis to build the +repository. For that just head to your [Travis-CI Profile +:ext](https://travis-ci.org/profile) and enable the repository. If it does not +show up yet, you can force a sync with the click of a button. + +## Access Credentials + +So how do you savely provide your credentials? Lektor accepts username and +password for the `ghpages+https` transport via the `LEKTOR_DEPLOY_USERNAME` +and `LEKTOR_DEPLOY_PASSWORD` environment variables. These can be set in the +Travis-CI settings of your repository on travis-ci.org in secret so they are +not stored anywhere else and will not show up in the build output. However one +thing you need to be careful with is that they still give access to your entire +account! + +To solve this problem we recommend two things: + +1. [Create a personal access token :ext](https://github.com/settings/tokens) + and use that instead. Just provide the token instead of your password on + sign-in. This makes it easily possible to just revoke that token if + something goes wrong. Note that you only need the `repo` scope for this + to work. This also works if you have 2FA activated on an account. +2. [Create a deployment (machine) user + :ext](https://developer.github.com/guides/managing-deploy-keys/#machine-users). + This allows you to use a user that is exclusively used for just the + purpose of updating the website. + +Once you have done that travis will start deploying the website on every +commit. + +## Committer Information + +By default the commits to the `gh-pages` branch will be authored by a user +named “Lektor Bot”. If you want to override this you can export the +`GIT_COMMITTER_NAME` and `GIT_COMMITTER_EMAIL` environment variables and +set them to something else. This is best done in the travis settings. + +## Private Repositories + +If you are using private repositories you will need the commercial version of +travis. It has the advantage that you can also set up SSH keys on there which +means that authentication becomes easier. For more information see [Private +Dependencies :ext](https://docs.travis-ci.com/user/private-dependencies/) in +the Travis CI documentation. + +## Speeding up Builds with Caching + +In the default setting Travis will have to rebuild everything because between +builds it does not cache the build results. You can change this by enabling +caching. Adjust your `.travis.yml` file to look like this: + +```yaml +language: python +python: 2.7 +cache: + directories: + - $HOME/.cache/pip + - $HOME/.cache/lektor +install: "pip install Lektor" +script: "lektor build && lektor deploy ghpages" +``` diff --git a/content/docs/guides/blog/contents.lr b/content/docs/guides/blog/contents.lr new file mode 100644 index 00000000..5c145496 --- /dev/null +++ b/content/docs/guides/blog/contents.lr @@ -0,0 +1,161 @@ +title: Blog +--- +summary: Shows how to build a basic blog with Lektor. +--- +body: + +Blogging is what spawned many content management systems and static site +generators. We're living in a world where many people like to share what's +on their minds and the best way to do that turns out to be pages ordered by +the date of publication. That's basically what a blog is. Here we will +build our own blog with Lektor. + +## The Models + +For a blog we want two models. The model for the `blog` itself and the model +for the `blog-post`. The idea is that we have a folder called `blog` which +uses the `blog` model which contains all the blog posts. Each of the blog +posts will use the `blog-post` model. + +### `blog.ini` + +This is the model for the blog itself. It instructs Lektor that all of the +pages below it will be blog posts, how many blog posts we want to show per +page and what the order is. We also set it to `hidden` and `protected` which +will make it unavailable in the admin (`hidden`) for new pages and make it +impossible to delete (`protected`). This means we need to manually create the +one page later which will use this. + +```ini +[model] +name = Blog +label = Blog +hidden = yes +protected = yes + +[children] +model = blog-post +order_by = -pub_date, title + +[pagination] +enabled = yes +per_page = 10 +``` + +### `blog-post.ini` + +Each blog post has a title, publication date, author and body. The publication +date and title are also used for sorting if you look into the `blog.ini`. +Lastly we set up the label of the page to be the title of the blog post. We +can also set it to `hidden` as the model is automatically selected in the +admin whenever a page is created in the blog. + +```ini +[model] +name = Blog Post +label = {{ this.title }} +hidden = yes + +[fields.title] +label = Title +type = string +size = large + +[fields.pub_date] +label = Publication date +type = date +width = 1/2 + +[fields.author] +label = Author +type = string +width = 1/2 + +[fields.body] +label = Body +type = markdown +``` + +## The Templates + +Now that we have the models set up, we want to create the templates. + +### `blog.html` + +Let's start with the blog overview page. This template is used for our `blog` +model. In this example we just want to show the titles of the post on the +overview page with the author and date and a controller for the pagination. +Because pagination is enabled we can iterate over `this.pagination.items` +instead of `this.children` which will return only the items for the intended +page. + +```html+jinja +{% extends "blog_layout.html" %} +{% from "macros/pagination.html" import render_pagination %} +{% block title %}My Blog{% endblock %} +

My Blog

+ +
    + {% for post in this.pagination.items %} +
  • + {{ post.title }} — + by {{ post.author }} + on {{ post.pub_date|dateformat }} + {% endfor %} +
+ + {% if this.pagination.pages > 1 %} + {{ render_pagination(this.pagination) }} + {% endif %} +{% endblock %} +``` + +For the pagination macro have a look at the [pagination guide +:ref](../pagination/) which covers that part. + +### `blog-post.html` + +Now we just need a template for our blog post. This (`blog-post.html`) will +do: + +```html+jinja +{% extends "layout.html" %} +{% block title %}{{ this.title }} | My Blog{% endblock %} +{% block body %} + <1h>{{ this.title }} +

+ by {{ this.author }} + on {{ this.pub_date|dateformat('full') }} +

{{ this.body }}
+{% endblock %} +``` + +## Initializing The Blog + +Now that we have models and templates we just need to designate a part of the +website as blog. For that create a new folder in your `content/` folder +with the name of your blog. For instance just `content/blog` and put a +`contents.lr` file with this content in: + +``` +_model: blog +``` + +Now you can head to the admin to create new blog posts. + +## Changing the URL Structure + +With the above settings the blog will live at `blog/` and the posts at +`blog/`. But what if you want to put the date of the blog post +into the URL? That's thankfully very easy. All you need to do is to set up +a new URL format for the children. Just edit `blog.ini` and add this to +the `[children]` section: + +```ini +slug_format = {{ (this.pub_date|dateformat('YYYY/M/') if this.pub_date) ~ this._id }} +``` + +What this does is that it will prepend the year (`YYYY`) and month (`M`) to +the ID of the page if the publication date is configured. Otherwise it will +just use the ID of the page. With this change our blog post will move from +for instance `blog/hello/` to `blog/2015/12/hello/`. diff --git a/content/docs/guides/categories/contents.lr b/content/docs/guides/categories/contents.lr new file mode 100644 index 00000000..c3ce2037 --- /dev/null +++ b/content/docs/guides/categories/contents.lr @@ -0,0 +1,211 @@ +title: Categories +--- +summary: A simple pattern for implementing categories. +--- +body: + +Because Lektor is based on the idea of mirroring the tree structure from the +file system to the final website it sometimes might not be quite clear how +one could achieve any form of filtering (for instance categories). However +this is actually very easily achieved through the concept of “child +replacement”. This will give you a quick introduction to how this feature +can be used to implement categories. + +## Basic Setup + +In this case we assume you have a bunch of projects that can exist in +different categories and it should be possible to see which projects belong +to which category. + +## The Models + +So we will end up with four models: `projects.ini`, `project.ini`, +`project-categories.ini` and `project-category.ini`. Here is how they look: + +### `projects.ini` + +```ini +[model] +name = Projects +label = Projects +hidden = yes +protected = yes + +[children] +model = project +order_by = -date, name +``` + +### `project.ini` + +```ini +[model] +name = Project +label = {{ this.name }} +hidden = yes + +[fields.name] +label = Name +type = string + +[fields.date] +label = Date +type = date + +[fields.description] +label = Description +type = markdown + +[fields.categories] +label = Categories +type = checkboxes +source = site.query('/project-categories') +``` + +The above models should be mostly clear. What is probably not entirely clear +is the `source` parameter for the categories. Esentially we instruct the +admin panel to fill the selection for the checkboxes from the child pages +below the `project-categories` folder. This is where we will maintain the +categories. + +### `project-categories.ini` + +```ini +[model] +name = Project Categories +label = Project Categories +hidden = yes +protected = yes + +[children] +model = project-category +order_by = name +``` + +### `project-category.ini` + +```ini +[model] +name = Project Category +label = {{ this.name }} +hidden = yes + +[children] +replaced_with = site.query('/projects').filter(F.categories.contains(this)) + +[fields.name] +label = Name +type = string +``` + +So this is where the magic lives. the `replaced_with` key in the `[children]` +section tells Lektor to ignore the child pages that would normally exist below +the path but to substitute them with another query. In this case we replace +them with all the projects where the category (`this`) is in the checked +set of categories (`F.categories`). + +## Initializing The Folders + +Now that we have the models, we need to initialize the folders. This is +necessary because most models are hidden which means they cannot be selected +from the admin interface. We need the following things: + +### `projects/contents.lr` + +``` +_model: projects +``` + +### `project-categories/contents.lr` + +``` +_model: project-categories +---- +_slug: projects/categories +``` + +Here we also tell the system that we want to move the page from +`project-categories/` to `projects/categories/` which looks nicer. This +means a category named `Foo` will then end up as `projects/categories/foo/`. + +## Creating Categories + +Now we can head to the admin panel to create some categories. Each category +that is created will become available for new projects in the category +selection. + +## The Templates + +To render out all the pages we probably want to use some macros to reuse +markup that appears on multiple pages. + +### `projects.html` + +```html+jinja +{% extends "layout.html" %} +{% from "macros/projects.html" import + render_project_list, render_category_nav %} +{% block title %}Projects{% endblock %} +{% block body %} +

Projects

+ {{ render_category_nav(active=none) }} + {{ render_project_list(this.children) }} +{% endblock %} +``` + +### `project.html` + +```html+jinja +{% extends "layout.html" %} +{% block title %}{{ this.name }}{% endblock %} +{% block body %} +

{{ this.name }}

+
{{ this.description }}
+{% endblock %} +``` + +### `project-categories.html` + +```html+jinja +{% extends "layout.html" %} +{% from "macros/projects.html" import render_category_nav %} +{% block title %}Project Categories{% endblock %} +{% block body %} +

Project Categories

+ {{ render_category_nav(active=none) }} +{% endblock %} +``` + +### `project-category.html` + +```html+jinja +{% extends "layout.html" %} +{% from "macros/projects.html" import render_category_nav %} +{% block title %}Project Category {{ this.name }}{% endblock %} +{% block body %} +

Project Category {{ this.name }}

+ {{ render_category_nav(active=this._id) }} + {{ render_project_list(this.children) }} +{% endblock %} +``` + +### `macros/projects.html` + +```html+jinja +{% macro render_category_nav(active=none) %} +
    + {% for category in site.query('/project-categories') %} + {{ category.name }} + {% endfor %} +
+{% endmacro %} + +{% macro render_project_list(projects) %} + +{% endmacro %} +``` diff --git a/content/docs/guides/contents.lr b/content/docs/guides/contents.lr new file mode 100644 index 00000000..f5655c03 --- /dev/null +++ b/content/docs/guides/contents.lr @@ -0,0 +1,14 @@ +title: Guides +--- +summary: Guides with solutions for common problems. +--- +sort_key: 100 +--- +allow_comments: no +--- +body: + +Because Lektor is quite a generic system, sometimes I might not be quite +obvious what the best course of action is. This part of the documentation +contains some guides that should help find inspiration for solving certain +problems. diff --git a/content/docs/guides/disqus/contents.lr b/content/docs/guides/disqus/contents.lr new file mode 100644 index 00000000..bfc43891 --- /dev/null +++ b/content/docs/guides/disqus/contents.lr @@ -0,0 +1,48 @@ +title: Disqus Comments +--- +summary: Enabling disqus comments in one minute. +--- +body: + +The `disqus-comments` plugin adds support for Disqus comments to Lektor and +with it's help you can have comments on your pages in no time. Once the plugin +is enabled a `render_disqus_comments` function is available for templates which +can render a disqus comment box. All you need to do for this is to create your +own Disqus community at [Disqus Engage :ext](https://publishers.disqus.com/engage). + +Once you have created your community there you will be given a "short name" +which can then be used with this plugin. + +## Enabling the Plugin + +First you need to enable the plugin. The following command will do that +for you: + +``` +$ lektor plugins add disqus-comments +``` + +## Configuring the Plugin + +The plugin has a config file that is needed to inform it about your +website. Just create a file named `disqus-comments.ini` into your +`configs/` folder and configure the `shortname` key with the name of +your disqus community: + +```ini +shortname = YOUR_SHORTNAME +``` + +## In Templates + +Now you can add a discussion box to any of your templates by just using +the `render_disqus_comments` function. Just calling it is enough to +get the comment box: + +```html +
{{ render_disqus_comments() }}
+``` + +Optionally the function accepts two arguemnts: `identifier` and +`url` to override the defaults. For more information have a look at +the disqus widget documentation. diff --git a/content/docs/guides/pagination/contents.lr b/content/docs/guides/pagination/contents.lr new file mode 100644 index 00000000..c147d462 --- /dev/null +++ b/content/docs/guides/pagination/contents.lr @@ -0,0 +1,71 @@ +title: Pagination +--- +summary: Shows how to build a pagination with Lektor +--- +body: + +When you have too many items to show on one page you might want to use +Lektor's built-in pagination support. It allows a page to show only a +subset of the child records per page. + +## Configuring Pagination + +First you need to enable the pagination in the model. Primarily you need +to enable the pagination and set how many items show up on a page. Just +add this to the parent model: + +```ini +[pagination] +enabled = yes +per_page = 10 +``` + +## Selecting the Children + +Now that you have the pagination configured you need to iterate only over +the children of an active page in your template rather than the children of the +entire record. This can be done by changing `this.children` to +`this.pagination.items`: + +```html+jinja +{% for child in this.pagination.items %} + ... +{% endfor %} +``` + +## Rendering a Pagination + +Lastly we need to render the pagination somehow. This is up to you. The +handy [pagination object :ref](../../api/db/record/pagination/) has a few +very useful attributes that can be used for rendering. It's recommended +to make a `macros/pagination.html` that looks something like this so that +you can render the same pagination everywhere: + +```html+jinja +{% macro render_pagination(pagination) %} + +{% endmacro %} +``` + +## Using the Macro + +Now that you have that set up, you can use the macro like this: + +```html+jinja +{% from "macros/pagination.html" import render_pagination %} +{% if this.pagination.pages > 1 %} + {{ render_pagination(this.pagination) }} +{% endif %} +``` diff --git a/content/docs/guides/portfolio/contents.lr b/content/docs/guides/portfolio/contents.lr new file mode 100644 index 00000000..ba70c386 --- /dev/null +++ b/content/docs/guides/portfolio/contents.lr @@ -0,0 +1,164 @@ +title: Portfolio Sites +--- +summary: Demonstrates how to build portfolio sites with Lektor. +--- +body: + +Because Lektor lets you customize your data model entirely it's the perfect +tool for building portfolio websites. No matter what it is you're building, +you can structure the data exactly like you want. In this example we will +assume we build a website for someone who wants to showcase their art projects. + +## The Models + +The way we want to go about this is that we have a site where all the +projects shows up and then a detail page for each project in particular. + +### `projects.ini` + +First we set up the project overview page. This model instructs Lektor that +all of the pages below it will be of type `project`. We also set it to +`hidden` and `protected` which will make it unavailable in the admin (`hidden`) +for new pages and make it impossible to delete (`protected`). This means we +need to manually create the one page later which will use this. + +Because we only have a single page for the projects overview we give it a +label manually (`label = Projects`). + +```ini +[model] +name = Projects +label = Projects +hidden = yes +protected = yes + +[children] +model = project +order_by = -date, name +``` + +### `project.ini` + +Next up is the model for the project. This is completely up to you, we will go +with some things here that might appear on such a portfolio page. In addition +we will do something with the attachments of this page, but more about that +later. For now we just order them by their filename (`_id`): + +```ini +[model] +name = Project +label = {{ this.name }} +hidden = yes + +[attachments] +order_by = _id + +[fields.name] +label = Name +type = string +size = large + +[fields.date] +label = Date +type = date +size = 1/4 + +[fields.type] +label = Project type +type = string +size = 1/4 + +[fields.website] +label = Website +type = url +size = 1/2 + +[fields.description] +label = Description +type = markdown +``` + +## Templates + +So now that we have models, we should probably go over what we can do with the +attachments. Because each page in Lektor can have attachments added we can +automatically reference those in our templates. Here is what we're going to +do: we will take all the attached images, order them by their filename and then +render thumbnails for them in the detail page. Additionally we want to show +one of the images on the overview page if it's available. + +### `projects.html` + +So here we just render out all projects in the order defined in our +models and if there is an image attached, we pick the first one and make +a thumbnail. + +```html+jinja +{% extends "layout.html" %} +{% block title %}Projects{% endblock %} +{% block body %} +

Projects

+
+ {% for project in this.children %} +
+ {% set image = project.attachments.images.first() %} + {% if image %} + + {% endif %} +

{{ project.name }} + ({{ project.date.year }})

+

{{ project.type }}

+
+ {% endfor %} +
+{% endblock %} +``` + +### `project.html` + +For the detail page we show all information we know about: + +```html+jinja +{% extends "layout.html" %} +{% block title %}{{ this.name }} ({{ this.date.year }}){% endblock %} +{% block body %} +

{{ this.name }}

+
+
Date +
{{ this.date|dateformat }} + {% if this.website %} +
Website +
{{ this.website.host }} + {% endif %} +
Project type +
{{ this.type }} + +

Description

+
{{ this.description }}
+ {% set images = project.attachments.images.all() %} + {% if images %} +

Images

+ {% for image in images %} +
+ + {% if image.exif %} +

+ {{ image.exif.camera }} + {% if image.exif.created_at %} + ({{ image.exif.created_at|dateformat }}) + {% endif %} + {% endif %} +

+ {% endfor %} + {% endif %} +{% endblock %} +``` + +Some notes on what's maybe not entirely obvious: + +* a `url` field does not just give access to the stored URL but also provides + some properties such as `.host` to just get the host of the website. +* we can use the `|dateformat` filter to format out dates nicely +* by calling `.all()` on our images we get the images back as a list where we + can then check if any images exist with `if images`. +* we can access embedded EXIF information by using the `.exif` property. diff --git a/content/docs/guides/single-page/contents.lr b/content/docs/guides/single-page/contents.lr new file mode 100644 index 00000000..00c345bc --- /dev/null +++ b/content/docs/guides/single-page/contents.lr @@ -0,0 +1,136 @@ +title: Single-Page +--- +summary: Some inspiration of how to structure single page apps. +--- +body: + +Lektor builds a page out of every source file. However that does not mean +that you cannot have multiple pages combined into a single one. You have +probably already seen that when doing something with the children of a page +in templates. But you can do this in much more elaborate ways. + +You could for instance generate a huge page out of individual pages to +achieve a page where you can scroll to all parts. + +In this example we will implement a project documentation where all +documentation pages are rendered into a long page. + +## The Models + +First we need to define the models. We will need three here: + +* `doc-pages.ini`: this is a dummy model that we will use to hide away the + parts of our big page. +* `doc-page.ini`: this is the model for our documentation pages. +* `index.ini`: this is the model we just use to combine everything together. + +We will just sort all the documentation pages by the page ID so it's possible +to just order the pages by prefixing the page with a number +(`0001-installation`, `0002-quickstart` etc.) + +### `index.ini` + +```ini +[model] +name = Documentation +label = {{ this.title }} +hidden = yes +protected = yes + +[fields.title] +type = string +``` + +### `doc-pages.ini` + +```ini +[model] +name = Documentation Pages +label = Documentation Pages +hidden = yes +protected = yes + +[children] +model = doc-page +order_by = _id +``` + +### `doc-page.ini` + +```ini +[model] +name = Documentation Page +label = {{ this.title }} +hidden = yes + +[fields.title] +type = string + +[fields.body] +type = markdown +``` + +## Initializing the Contents + +Now that we have the models we need to set up the initial pages. All our +models are hidden which means that we cannot use the admin panel to setup +the initial pages. So we need to set up the structure outselves. The +following pages and contents we will need: + +### `contents.lr` + +``` +_model: index +---- +title: My Documentation +``` + +### `doc/contents.lr` + +``` +_model: doc-pages +---- +_hidden: yes +``` + +This will set up our index model and our doc-pages model for the `docs/` +folder. The latter is also set to `_hidden` which will make Lektor prevent +the generation of those files: they are invisible. So we need to find other +ways to render them. + +## Templates + +For now we only need a single template: `index.html`, the one that will be +used by the only page that actually renders. Within that template we now +need to query for all the other pages we have below `doc/`: + +```html+jinja +{% extends "layout.html" %} +{% block title %}{{ this.title }}{% endblock %} +{% block body %} + {% set pages = site.query('/doc').all() %} +
+

{{ this.title }}

+ +
+ {% for page in pages %} +
+

{{ page.title }}

+ {{ page.body }} +
+ {% endfor %} +{% endblock %} +``` + +## Other Things To Do + +Now every time you add or change a page below `doc/` it will start rebuilding +the overview page. Obviously you can further complicate the setup by doing +something with attachments, supporting different models etc. The +possibilities are endless. diff --git a/content/docs/guides/sitemap/contents.lr b/content/docs/guides/sitemap/contents.lr new file mode 100644 index 00000000..36933087 --- /dev/null +++ b/content/docs/guides/sitemap/contents.lr @@ -0,0 +1,67 @@ +title: Sitemap +--- +summary: Quick demo of how to build a custom sitemap.xml. +--- +body: + +If you want to have a `sitemap.xml` file for search engines this is something +you can very easily create yourself. All you need for that is a contents file +and a custom template. + +## Contents File + +First we need to create a contents file. Since `sitemap.xml` always goes +into the same location we create a folder called `sitemap.xml` inside our +`content` folder and add a `contents.lr` file with the following data: + +``` +_template: sitemap.xml +---- +_model: none +``` + +This instructs Lektor to use the template `sitemap.xml` for this page. We +also give it the empty `none` model for good measure. + +## Template File + +The template loaded will be `templates/sitemap.xml`. In this file we just +iterate over all pages of the site recursively. This also automatically +skips hidden pages so those will not be generated out. + +```xml+jinja + + + {%- for page in [site.root] if page != this recursive %} + {{ page|url(external=true) }} + {{- loop(page.children) }} + {%- endfor %} + +``` + +Note that because sitemaps need to have external URLs (with scheme and +everything) you will need to configure the `url` of the site before the +template starts working. For more information see [Project File +:ref](../../project/) + +## Human Readable Sitemap + +But what if you want a beautiful sitemap as a tree for human reading? This is +not any harder. Instead of making a `sitemap.xml/contents.lr` file just +create a `sitemap/contents.lr` file instead and use a template like +`sitemap.html`. Then use something like this: + +```html+jinja +{% extends "layout.html" %} +{% block title %}Sitemap{% endblock %} +{% block body %} +
    + {% for page in [site.root] recursive if page.record_label %} +
  • {{ page.record_label }} + {% if page.children %} +
      {{ loop(page.children) }}
    + {% endif %} + {% endfor %} +
+{% endblock %} +``` diff --git a/content/docs/guides/webpack/contents.lr b/content/docs/guides/webpack/contents.lr new file mode 100644 index 00000000..0a688e8b --- /dev/null +++ b/content/docs/guides/webpack/contents.lr @@ -0,0 +1,155 @@ +title: Webpack +--- +summary: Shows how to use webpack to Sass, Less or other things with Lektor. +--- +body: + +Websites are exploding in complexity and even static websites are no exception +to this. In particular systems like bootstrap and friends no longer just come +in CSS files but they come with their own build setup to ship JavaScript files, +they use Less or Sass to build the stylesheets and much more. + +Right now there is a fight between different systems to figure out which way +the journey will go, but one of the best solutions has turned out to be +[webpack :ext](https://webpack.github.io/). + +Webpack is not natively supported by Lektor but there is an official [Webpack +Support Plugin :ext](https://github.com/lektor/lektor-webpack-support) which +can be used to make Lektor and Webpack friends. + +## Enabling the Plugin + +First you need to enable the plugin. The following command will do that +for you: + +``` +$ lektor plugins add webpack-support +``` + +## Webpack Setup + +Now you need to configure webpack. The plugin expects a webpack project in the +`webpack/` folder. Within you will need a `package.json` as well as a +`webpack.config.js` + +### `package.json` + +This file instructs `npm` which packages we will need. All we need for a +start is to create an almost empty file: + +```json +{ + "private": true +} +``` + +Now we can npm install all the things we want: + +``` +$ npm install --save-dev webpack babel-core node-sass babel-loader sass-loader css-loader url-loader style-loader file-loader +``` + +This will install webpack itself together with babel and sass as well as +a bunch of loaders we need for getting all that configured. Because we +created a `package.json` before and we use `--save-dev` the dependencies +will be remembered in the `package.json` file. + +### `webpack.config.js` + +Next up is the webpack config file. Here we will go with a very basic +setup that's good enough to cover most things you will encounter. The +idea is to build the files from `webpack/scss` and `webpack/js` into +`assets/static/gen` so that we can use it even if we do not have webpack +installed for as long as someone else ran it before. + +In this example we will configure the following things: + +* all `.scss` files will be processed with Sass +* all `.js` files will be processed with Babel to convert ES6 into ES5 +* JS and CSS files will be minified +* all built files will go to `assets/static/gen` +* there will be a `gen/app.js` and a `gen/styles.css` file to include + +```javascript +var webpack = require('webpack'); +var path = require('path'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); + +module.exports = { + entry: { + app: './js/main.js', + styles: './scss/main.scss' + }, + output: { + path: path.dirname(__dirname) + '/assets/static/gen', + filename: '[name].js' + }, + devtool: '#cheap-module-source-map', + resolve: { + modulesDirectories: ['node_modules'], + extensions: ['', '.js'] + }, + module: { + loaders: [ + { test: /\.js$/, exclude: /node_modules/, + loader: 'babel-loader' }, + { test: /\.scss$/, + loader: ExtractTextPlugin.extract( + 'style-loader', 'css-loader!sass-loader') }, + { test: /\.css$/, + loader: ExtractTextPlugin.extract( + 'style-loader', 'css-loader') }, + { test: /\.(woff2?|ttf|eot|svg|png|jpe?g|gif)$/, + loader: 'file' } + ] + }, + plugins: [ + new ExtractTextPlugin('styles.css', { + allChunks: true + }), + new webpack.optimize.UglifyJsPlugin() + ] +}; +``` + +## Creating the App + +Now we can start building our app. We configured at least two files +in webpack: `js/main.js` and `scss/main.scss`. Those are the entry +points we need to have. You can create them as empty files in +`webpack/js/main.js` and `webpack/scss/main.scss`. + +## Running the Server + +Now you're ready to go. When you run `lektor server` webpack will not +run, instead you need to now run it with the `webpack` flag which +will enable the webpack build: + +``` +$ lektor server -f webpack +``` + +Webpack automatically builds your files into `assets/static/gen` and this is +where Lektor will then pick up the files. This is done so that you can ship +the webpack generated assets to others that do not have webpack installed which +simplifies using a Lektor website that uses webpack. + +## Manual Builds + +To manually trigger a build that also invokes webpack you can also pass +the `webpack` flag there: + +``` +$ lektor build -f webpack +``` + +## Including The Files + +Now you need to include the files in your template. This will do it: + +```html+jinja + + +``` diff --git a/content/docs/installation/contents.lr b/content/docs/installation/contents.lr new file mode 100644 index 00000000..41317094 --- /dev/null +++ b/content/docs/installation/contents.lr @@ -0,0 +1,67 @@ +title: Installation +--- +summary: How to install Lektor on your computer. +--- +sort_key: 50 +--- +body: + +Lektor comes in two flavors: the command line executable as well as as a +desktop application. The desktop version also contains the command line +executable however it bundles together all dependencies of Lektor in an +easy to use package which heavily simplifies installation. + +## Desktop Application + +Currently the desktop application is only available for OS X and can be +[downloaded from the Lektor website](../../downloads/). It comes as a downloadable +disk image that you can mount which contains one application by the name of +`Lektor.app`. Just drag it into your `Applications` folder and you are good to +go. + +If you also want access to the command line tools just launch `Lektor.app` +and then click on *Lektor ➤ Install Shell Command*. + +## Command Line Application + +If you do not want to install the desktop app you can just install the command +line executable. This one runs on more operating systems (OSX, Linux and +Windows) but that installation is a bit more involved. + +You need to make sure you have the following software installed on your computer: + +* Python 2.7 (**not** Python 3.x) +* ImageMagick (`brew install imagemagick` or `apt-get install imagemagick` + can get you this on OS X and Ubuntu respectively) + +Once you have that installed you can get Lektor installed with our +installation script: + +``` +$ curl -sf https://www.getlektor.com/install.sh | sh +``` + +Alternatively you can manually install it with `virtualenv` if you know +how that works: + +``` +$ virtualenv venv +$ . venv/bin/activate +$ pip install Lektor +``` + +## Development Version + +If you want to install the development version of Lektor you can do so. It's +the same as with installing the command line application but instead of +using PyPI you install directly from git and you need to have `npm` installed +to build the admin UI: + +``` +$ git clone https://github.com/lektor/lektor +$ cd lektor +$ make build-js +$ virtualenv venv +$ . venv/bin/activate +$ pip install --editable . +``` diff --git a/content/docs/models/attachments/contents.lr b/content/docs/models/attachments/contents.lr new file mode 100644 index 00000000..6cc093ce --- /dev/null +++ b/content/docs/models/attachments/contents.lr @@ -0,0 +1,15 @@ +title: Attachments +--- +summary: Describes behavior of attachments for records. +--- +body: + +Each source can also have other files attached. Anything that is not +the main content file in a folder is considered to be an attachment. +There are a few settings to control these in the `[attachments]` section: + +* `enabled`: if set to `no` then attachments are disabled. If they do + exist in the folder they are silently ignored. +* `model`: an optional default model that is used for all attachments. +* `order_by`: controls the ordering of attachments, similar to how this + works for child pages. diff --git a/content/docs/models/children/contents.lr b/content/docs/models/children/contents.lr new file mode 100644 index 00000000..7006101e --- /dev/null +++ b/content/docs/models/children/contents.lr @@ -0,0 +1,86 @@ +title: Children & Pagination +--- +summary: Explains how child records and pagination works. +--- +body: + +By default each model can have child pages. This is what enables the +hierarchical structure of the Lektor database. Children are configured +together with a model that encloses it. This is typically called the +“collection model”. For instance you can have a collection model called +`pages` which is the parent to a few `page` children. + +## Child Configuration + +Most configuration related to child pages goes into `[children]`. It +configures how children of the model should be handled. In particular it +controls if a page can have children to begin with, if the children can be of +any format or have to match specific models and more. + +Here are the most important options below `[children]`: + +- `enabled`: this can enable or disable children. The default is that a + page can have children. +- `slug_format`: this key controls the URL key for children. By default + the URL key is the ID of the page. However in some cases you might + want to change that. For instance blog-posts might want to pull in + parts of the date into the URL. This is a template expression. +- `model`: if this is set to a string, then all children are automatically + forced to the same model and the UI will not give a way to select a + model when creating a new child page. This allows specific parts of + the website to use the correct models automatically. For instance you + can force all pages below `/projects` to use the `project` model. +- `order_by`: a comma separated list of fields that indicate the default + sort order. If a field is prefixed with a minus sign, the order is + inversed. +- `replaced_with`: this allows a page to simulate that it has children + when it really has not. This can be a query expression and the result + is then used as the children of the model. This for instance can be + used to implement categories with filtering. + +## Child Slug Behavior + +Slugs are the URL paths or more correctly: parts of it. The URL paths +always are the concatenation from the parent's page URL path plus the +children's slug. If not configured the default slug of children is the +page's `_id`. A slug can contain slashes to navigate into folders. This +also allows pages to overlap into other pages. For instance if you have +a model called `categories` which is used by a folder named `categories/`, +that folder could set the `_slug` to `blog/categories` and then the URL +for categories would be `blog/categories/example` instead of +`categories/example`. + +The default slug can be changed with the `slug_format` parameter in the +`[children]` section which can be a template expression. For instance a +common way to format slugs would be to include some date components. What's +important about this is that the slug expression must not fail even if fields +are empty! This is necessary because new pages will start out with the +fields not being filled in. + +This for instance includes a date in the URL if set: + +```ini +slug_format = {{ (this.date|dateformat('YYYY/M/') if this.date) ~ this._id }} +``` + +## Pagination + +In general a source document renders into a single page. The exception to +that rule are pages with children which show the children on the rendered +page and have pagination enabled. In that case it becomes possible to +slide the range of children into smaller pieces and render those slides +instead. + +Pagination is controlled from the `[pagination]` section. The following +keys are available: + +* `enabled`: if set to `yes` pagination becomes enabled. It's off by + default. +* `per_page`: this controls how many children are are shown per page. If + pagination is enabled and this is not set, an implicit default of `20` + is assumed. + +If pagination is enabled then the [pagination attribute +:ref](../../api/db/record/pagination/) of a record becomes available. For +more information have a look at the [pagination guide +:ref](../../guides/pagination/). diff --git a/content/docs/models/contents.lr b/content/docs/models/contents.lr new file mode 100644 index 00000000..85014dc6 --- /dev/null +++ b/content/docs/models/contents.lr @@ -0,0 +1,95 @@ +title: Data Modelling +--- +summary: Gives a basic introduction to creating data models for Lektor. +--- +sort_key: 200 +--- +body: + +What makes Lektor so powerful is the ability to model your data and to then use +this data to generate the final results. Getting this part right will make it +easier later to generate beautiful looking HTML. + +## Models + +Models are the blueprints for your pages. They define which fields exist and +what goes into them. Models are stored in the `models` folder in your project +and are basic INI files. Models can have any name but if no model has been +explicitly selected, the model with the name `page` will be used. So having +a model with that name is always a good idea. + +Here is an example of a very basic model (`models/page.ini`): + +```ini +[model] +name = Page +label = {{ this.title }} + +[fields.title] +label = Title +type = string +size = large + +[fields.body] +label = Body +type = markdown +``` + +In this particular case we have a model with the id `model` (as defined by the +filename) and a name `Page` which will appear like that in the UI. Pages that +use this model will use the template expression `{{ this.title }}` to be +displayed in the UI. In this case it uses the title of the page. + +There are two fields defined: a `title` and a `body`. The former is just an +unformatted string which is show larger in the UI (`size = large`) and the +latter uses markdown for rendering. This will give it a text area in the admin +panel. + +## Fields + +Fields for models are ordered in the UI in the order they appear in the model. +Most options in the field are specific to the type that is selectd, but some +are the same for all of them. + +Fields not only define the behavior of the data (for instance strings and +integers are sorted differently) but also how it's shown in the UI and what +can be done with it in general. + +The following options are used for all types: + +- `label`: the label for the field. This is shown in the UI in larger letters +- `description`: an optional string that provides some description for the + field that is shown in the UI to give a bit more explanation. +- `addon_label`: an optional string that is supported by all types that are + rendered as an input field. This string is shown as a UI label on the + right side of the input field to give it more context. For instance it can + be used to clarify units of a field (pixel, percent etc.). +- `width`: defines the width of the input in the admin as a fraction. For + instance `1/4` sets it to a quarter of the width, `1/2` to a half etc. +- `size` can be set to `normal`, `small` or `large` to affect the size a + field is rendered in the admin UI. +- `type`: defines the type of the field. Depending on the type more options + can become available. + +There are many different field types that are available and they are documented +extensively in the [types documentation :ref](../api/db/types/). + +## Model Options + +Models have the following options that can cutomize the model itself: + +- `name`: the name of the model itself. Usually a more capitalized form of + the filename which is the ID of the model. +- `label`: a template expression that should be used for pages that use this + model. Typically this expression refers to the title but not always. For + instance blog posts might also want to refer to the date. +- `hidden`: a boolean value that indicates if the model should be hidden from + the UI or not. If set to `yes` then new pages cannot select this model. + This is very useful for models that are implied through configuration. +- `protected`: if a model is set to protected then all of it's instances + cannot be deleted once created. +- `inherits`: if you want to inherit all fields from another model then this + can be set to the name of another model. + +In addition to that, there are some configuration sections in the model file +that can customize more behavior. diff --git a/content/docs/models/flow/contents.lr b/content/docs/models/flow/contents.lr new file mode 100644 index 00000000..e7f78330 --- /dev/null +++ b/content/docs/models/flow/contents.lr @@ -0,0 +1,57 @@ +title: Flow Block Models +--- +summary: Explains how to model flow blocks. +--- +body: + +To use [Flow :ref](../../flow/) you need to define flow block models. +If you are not familiar with Flow yet, you should read the [Introduction +Documentation](../../flow/) to Flow first. + +## Defining Models + +Flow block models work pretty much exactly the same as [Regular Models +:ref](../). The differences are mostly minor and of cosmetic nature. They +are stored in the `flowblocks/` folder and are ini files just like models. + +Instead of using `[models]` as section, the section is called `[block]`. + +Here a very basic flow block model `flowblocks/text.ini`: + +```ini +[block] +name = Text Block +button_label = Text + +[fields.text] +label = Text +type = markdown + +[fields.class] +label = Class +type = select +choices = default, centered +choice_labels = Default, Centered +default = default +``` + +This should be self explanatory. One thing that is different about blocks +compared to regular models is that they support the `button_label` attribute +which an be used to customize the label of the button that adds blocks to +a flow. + +## Templates + +Now that we have a model for our flow block, we need to create a template +for it. When a flow block is added to a Flow, it will automatically +render it by default through the template `blocks/NAME.html` (so in our +case `blocks/text.html`). Here is a suitable template for this: + +```html+jinja +
+ {{ this.text }} +
+``` + +If you need to access the page the flow is used by, you can use the `record` +template variable. diff --git a/content/docs/plugins/contents.lr b/content/docs/plugins/contents.lr new file mode 100644 index 00000000..bea34a7e --- /dev/null +++ b/content/docs/plugins/contents.lr @@ -0,0 +1,40 @@ +title: Plugins +--- +sort_key: 120 +--- +summary: Explains how to use plugins and how to develop them. +--- +body: + +Lektor can be extended through the use of plugins. This is something that +can be done on a per-project basis. This gives a quick overview of how +to use plugins and how to build your own. + +## Loading Plugins + +Plugins can be added to a Lektor project in two different ways: + +1. Plugins can be added to the `[packages]` section of the project. In that + case Lektor will automatically download and enable the plugin next time + you build the project or run the server. +2. Plugins can be added to the `packages/` folder in your project. Each + plugin has to go into a separate folder. This method is especially useful + for project specific plugins or for plugin development. + +## Installing Plugins + +For completely automated plugin managment just open your project file in a +text editor and edit or extend the `[packages]` section. Just add a line +for each plugin in the form `name = version`: + +```ini +[packages] +lektor-cool-plugin = 1.0 +lektor-other-plugin = 1.2 +``` + +It's also possible to use the [plugins add :ref](../cli/plugins/add/) command +to automatically add the latest version of a plugin to the project file. + +To upgrade a plugin just increase the version number to the release you want +and Lektor will do the rest. diff --git a/content/docs/plugins/dev/contents.lr b/content/docs/plugins/dev/contents.lr new file mode 100644 index 00000000..58ca1c82 --- /dev/null +++ b/content/docs/plugins/dev/contents.lr @@ -0,0 +1,159 @@ +title: Development +--- +summary: A quick introduction to plugin development. +--- +sort_key: 10 +--- +body: + +If you want to dive into plugin development yourself, this guide will get you +going quickly. Before we can get started you need to have a Lektor project +we can temporarily add the plugin to and you need to have the `lektor` +[command line tool](../../cli/) installed. + +## Enable Development Mode + +When developing plugins it's very useful to enable Lektor's development +mode before starting the server. This can be achieved by exporting the +`LEKTOR_DEV` environment variable and setting it to `1`: + +``` +$ export LEKTOR_DEV=1 +$ lektor server +``` + +With that in place, Lektor will automatically restart the development server +when the plugin is changing. + +## Creating A Package + +Plugins come in packages. To make one, just create a folder with a sensible +name (typically the name of your plugin minus the `lektor-` prefix) in your +`packages/` folder. + +You can either do this manually or you can use the `lektor dev new-plugin` +command (see [new-plugin :ref](../../cli/dev/new-plugin/)) which will create +this folder structure for you: + +``` +$ lektor dev new-plugin +``` + +This will guide you through a flow which ends up creating a new plugin +package in the packages folder. + +Alternatively you can manually create a `packages/hello-world/` folder. + +Once that is done, we need to create a `setup.py` file which tells Lektor +what your plugin needs to run. This will already be created for you if +you used the wizard. + +```python +from setuptools import setup + +setup( + name='lektor-hello-world', + version='0.1', + py_modules=['lektor_hello_world'], + entry_points={ + 'lektor.plugins': [ + 'hello-world = lektor_hello_world:HelloWorldPlugin', + ] + }, + install_requires=[] +) +``` + +So going line by line, these are what the things mean: + +* `setuptools` is a module that helps us install the package with the + Python interpreter that Lektor uses. We only need the setup function + from it for this example. +* `name` is the name of the plugin when it's published to the Python package + index where all Lektor plugins go. As such it should be prefixed with + `lektor-` to make it not clash with other packages on the index. +* `version` identifies the version. During local development it does not + matter what you write here, but it will play a role once you start + publishing your packages. Users need to reference exact versions of these + plugins when using them. +* `py_modules`: this is a list of modules that your plugin needs to run. + This should always be exactly one module named `lektor_XXX` where `XXX` + is your plugin name with underscores instead of dashes as separators. + If you need more than one module you should use a package instead. This is + not covered here, but you can find this in the [setuptools documentation + :ext](https://pythonhosted.org/setuptools/). +* `entry_points`: this is meta data that is needed to associate our package + with Lektor. Lektor will load all plugins in the `lektor.plugins` list. + It can be a list of definitions in the form `plugin-name = import_path`. + The plugin name is what will show up in the plugin list in Lektor, + the import path should be the dotted import path to the module that contains + the plugin followed by a colon (`:`) with the class name afterwards. +* `install_requires`: this is a list of dependencies for our plugin. We + leave it empty here as we do not depend on anything in this simple + example. + +## Creating The Plugin + +Now it's time to create our first plugin that does absolutely nothing. We +create a new file with the name `lektor_hello_world.py` next to our +`setup.py` and put the following things in: + +```python +from lektor.pluginsystem import Plugin + +class HelloWorldPlugin(Plugin): + name = 'Hello World' + description = 'This is a demo plugin for testing purposes.' +``` + +If you now start your lektor server with `lektor server` you should +see some output that indicates that the plugin was loaded. You can also +get a list with `lektor plugins`: + +``` +$ lektor plugins +hello-world: Hello World + This is a demo plugin for testing purposes. + path: /Users/john/demo/packages/hello-world + import-name: lektor_hello_world:HelloWorldPlugin +``` + +## Hooking Events + +Plugins in Lektor are based on the concept of hooking events. There are many +events that can be hooked but we will only cover a very basic one here, +the `process_template_context` event. To respond to it, we need to implement +a function named `on_process_template_context`: + +```python +import random + +MESSAGES = [ + 'Reticulating splines', + 'Populating slots', + 'Possessing pawns', +] + +def get_random_message(): + return random.choice(MESSAGES) + +class HelloWorldPlugin(Plugin): + name = 'Hello World' + description = 'This is a demo plugin for testing purposes.' + + def on_process_template_context(self, context, **extra): + context['get_random_message'] = get_random_message +``` + +This will inject a function with the name `get_random_message` into our +template context whenever a template is rendered. This means that we +can access this function from templates then: + +```html+jinja +

Message of the page: {{ get_random_message() }} +``` + +## What Plugins Can Do + +To understand what you can do with plugins have a look at the +[Plugin API :ref](../../api/plugins/). diff --git a/content/docs/plugins/howto/contents.lr b/content/docs/plugins/howto/contents.lr new file mode 100644 index 00000000..2eb0ae01 --- /dev/null +++ b/content/docs/plugins/howto/contents.lr @@ -0,0 +1,113 @@ +title: Howto +--- +summary: Covers the most common thigns plugins would do and how to implement it. +--- +sort_key: 30 +--- +body: + +So you want to build a plugin but it's not quite clear how you would structure +a plugin to accomplish this? This part of the documentation should help you +find answers to those questions. + +## Expose Functionality + +Most plugins will provide functionality of sorts. There are two places where +functionality is typically needed: in templates or for other plugins to use. +Plugins can import from each other just like this, but functionality exposed +into templates should follow some basic rules: + +* modify `jinja_env.globals` or `jinja_env.filters` and do not use + `process-template-context` unless you absolutely know what you are doing. +* expose as few global variables as possible. If you have a lot you want to + provide then consider registering an object with the templates and to + attach multiple attributes on it. +* use clear names for template globals. This is a shared space that all + plugins and Lektor itself modify so do not provide a function called + `get_script` but for instance call it `get_my_plugin_script`. + +A simple example of a plugin that implements [Gravatar +:ext](https://en.gravatar.com/) support: + +```python +from hashlib import md5 +from werkzeug.urls import url_encode +from lektor.pluginsystem import Plugin + +BASE_URL = 'https://secure.gravatar.com/avatar/' + +def get_gravatar(email, **options): + fn = md5(email.lower().strip().encode('utf-8')).hexdigest() + return '%s/%s?%s' % (BASE_URL, fn, url_encode(options)) + +class GravatarPlugin(Plugin): + name = 'Gravatar' + def on_setup_env(self, **extra): + env.jinja_env.filters['gravatar'] = get_gravatar +``` + +## Configure Plugins + +Plugins can come with their own config files and it's encouraged that plugins +take advantage of this. Each plugin has exactly one INI file called +`.ini` inside the `configs` folder. + +To get access to the plugin the `get_config` function can be used which +returns a dict like object to access the ini file. + +```python +config = self.get_config() +value = config.get('section.key', 'default_value') +``` + +This would correspond to this config in `configs/my-plugin.ini`: + +```ini +[section] +key = the value +``` + +## Dependency Tracking + +While a lot of dependencies are tracked automatically, when you develop a +plugin you probably will discover that sometimes you need to track your own +ones. There are different ways in which dependency tracking can work and +depending on the situation you might have to use two different mechanisms. + +1. **Dependency tracking by association**: while a build of a source object + into an artifact is active more dependencies for that artifact can be + registered with the [record_dependency :ref](../../api/build/context/record-dependency/) + method of the context. It takes a filename that should be recorded as + additional dependency for the current artifact +2. **Dependency tracking as artifact source**: if you build your own artifact + you need to define the source files that make up the artifact (if you have + such a thing). For instance if you build a thumbnail you will need to + track those source files that are the source images. This can be done + through the [sub_artifact :ref](../../api/build/context/sub-artifact/) + method which declares a new artifact. + +Here examples for both of those in one: + +```python +import os +from flask import json +from lektor.pluginsystem import Plugin + +def dump_exif(image): + ctx = get_ctx() + path = posixpath.join(image.path, '-exif.json') + @ctx.sub_artifact(path, sources=[image.source_filename]) + def include_file(artifact): + ctx.record_dependency(__file__) + with artifact.open('wb') as f: + json.dump(image.exif.to_dict(), f) + return path + +class ExifDumpPlugin(Plugin): + def setup_env(self, **extra): + self.env.jinja_env.globals['dump_exif'] = dump_exif +``` + +This dumps out the EXIF data into a JSON file and returns the artifact name. +The source image is tracked as direct source for the artifact and within the +function we also track the plugin's filename to rebuild if the plugin changes. diff --git a/content/docs/plugins/list/contents.lr b/content/docs/plugins/list/contents.lr new file mode 100644 index 00000000..8dcbb78d --- /dev/null +++ b/content/docs/plugins/list/contents.lr @@ -0,0 +1,26 @@ +title: List +--- +summary: A list of known plugins. +--- +body: + +Lektor is a very young project so naturally not that many plugins exist yet. +This is a list of currently known plugins. + +## Official + +These are official plugins which means that they were developed by the +authors of Lektor and kept in good shape together with the rest of the +project: + +* [disqus-comments :ext](https://github.com/lektor/lektor-disqus-comments): + this plugin embeds disqus comments into your website. +* [webpack-support :ext](https://github.com/lektor/lektor-webpack-support): + adds support for building websites with webpack. + +## Inofficial + +There are currently no inofficial plugins listed here, but if you have some +you can [edit this page on github +:ext](https://github.com/lektor/lektor-website/edit/master/content/docs/plugins/list/contents.lr) +and send a pull request. diff --git a/content/docs/plugins/publishing/contents.lr b/content/docs/plugins/publishing/contents.lr new file mode 100644 index 00000000..a7ba1e9e --- /dev/null +++ b/content/docs/plugins/publishing/contents.lr @@ -0,0 +1,57 @@ +title: Publishing +--- +summary: Explains how publishing of plugins works. +--- +sort_key: 20 +--- +body: + +Once you are happy with a plugin you can publish it so that other people +can use it. Publishing of plugins happens through the +[Python Package Index :ext](https://pypi.python.org/) and can be +automatically done with the help of the lektor shell command. + +## Enhance your setup.py + +Before you can go about publishing your plugin there needs to be at least +some information added about it to your `setup.py`. At least the keys +`name`, `version`, `author`, `author_email`, `url` and `description` need to be +set. Here is a basic example of doing this: + +```python +from setuptools import setup + + +setup( + name='lektor-your-plugin', + author='Your Name', + author_email='your.email@your.domain.invalid', + version='1.0', + url='http://github.com/youruser/lektor-yourplugin', + license='MIT', + packages=['lektor_your_plugin'], + description='Basic description goes here', + entry_points={ + 'lektor.plugins': [ + 'hello-world = lektor_hello_world:HelloWorldPlugin', + ] + }, +) +``` + +## Publishing + +Once you augmented your `setup.py` you can go ahead with the publishing. First +you need to make sure you have a PyPI account. If you do not, you can +create one at [pypi.python.org :ext](https://pypi.python.org/pypi?%3Aaction=register_form). + +Once you have done that, you can publish the plugin from the command line +with the `lektor` command: + +``` +$ cd path/to/your/plugin +$ lektor dev publish-plugin +``` + +When you use this for the first time it will prompt you for your login +credentials for `pypi`. Next time it will have remembered them. diff --git a/content/docs/project/contents.lr b/content/docs/project/contents.lr new file mode 100644 index 00000000..c4a6ee99 --- /dev/null +++ b/content/docs/project/contents.lr @@ -0,0 +1,12 @@ +title: Project +--- +summary: Covers the general layout, configs and concepts behind Lektor projects. +--- +sort_key: 70 +--- +body: + +A project in the context of Lektor is a single website. It's typically a +folder that contains all the information necessary to build it. Within that +folder there are templates, models, content files as well as the main project +file which acts as configuration file. diff --git a/content/docs/project/file/contents.lr b/content/docs/project/file/contents.lr new file mode 100644 index 00000000..766aecf5 --- /dev/null +++ b/content/docs/project/file/contents.lr @@ -0,0 +1,153 @@ +title: Project File +--- +summary: Covers everything about the project file in Lektor. +--- +body: + +The project file holds the main configuration of the project and is used to +identify the project for the user interface. The project file is an INI file +and the minimal content is the name of the project: + +```ini +[project] +name = My Fancy Project +``` + +The name of the file can be arbitrary but must have the `.lektorproject` +extension or Lektor will not be able to find it. When Lektor looks for a +project it looks upwards from the current folder until it finds a single +file with the `.lektorproject` extension then that's the root of the project. + +## Config Sections + +Within the project file there are various configuration selections. The +following sections currently exist: + +### `[project]` + +This section controls some basics about the project: + +`name` +> This is the human readable name of the project. It's used in various +> places where the system wants to show the context of the operations. For +> instance the admin panel will display this to indicate which project is +> being worked on. + +`locale` +> This is the default locale of the website. This defaults to `en_US` and +> can be changed to many others. Most locales of the CLDR project are +> supported. This information is for instance used to format dates. + +`url` +> This is the full URL of the website. If set this information can be used +> to enable the `external` URL generation parameter. Lektor tries hard to +> make websites work in a way where this information is not necessary but +> some systems might need it. For instance sitemaps require full URLs and +> not having them would be a violation of the specification. + +`path` +> This setting can be used to configure a different path for the project +> tree. This requires a bit of explanation: +> +> If this is not set (which is the default) then Lektor will find the +> content files right next to the project file. However in some situations +> you might want to move a project file to a completely different location +> for instance because you want to have settings in there that you do not +> want to put into version control. In that case you can set the `path` +> in the file to a path (absolute or relative to the project file) which +> resolves to the project tree. +> +> Note that if this setting is used some functionality in the desktop app +> might no longer work (for instance opening `.lr` files with a double click). + +Example: + +```ini +[project] +name = My Website +url = https://www.mywebsite.invalid/ +locale = de_DE +``` + +### `[packages]` + +This section controls the packages (plugins) that should be installed for +this project. It's a simple key/value list where the key is the plugin +name and the value is the version number. + +Example: + +```ini +[packages] +lektor-webpack-support = 0.1 +``` + +### `[servers.*]` + +This section can be repeated and each instance sets up a server. The `*` +needs to be replaced with the ID of the server. This ID is used by the +command line tool to select the server to deploy to. For more information +about this see the [Deployment Guide :ref](../../deployment/) + +`name` +> Human readable name for this server (shown in the UI) + +`target` +> The target URL for the server. This URL is specific to the deployment +> method that is being used. For a list of which URLs are supported refer +> to the deployment guides. + +`enabled` +> This setting can be used to enable/disable the server. The default is `yes`. + +`default` +> This can be used to set a server to be used by default. If only one server +> is configured it's an implicit default. + +Example: + +```ini +[servers.production] +name = Production +enabled = yes +default = yes +target = rsync://server/path/to/folder +``` + +### `[alternatives.*]` + +This configures [Alternatives :ref](../../content/alts/). It is repeated for +each intended alternative. The default behavior is that alternatives are +disabled. + +`name` +> The human readable name for the alternative. The admin interface uses this. + +`url_prefix` +> A prefix that is added in front of all URLs to identify this alternative. + +`url_suffix` +> A suffix that is added behind all URLs to enable this alternative. This is +> currently discouraged compared to the URL prefix as it might not yet work +> in all situations properly. + +`primary` +> If this is set to `true` then the alternative is selected as primary. For +> more information about this refer to the guide. + +`locale` +> This setting can override the global site locale for a specific alternative. + +Example: + +```ini +[alternatives.en] +name = English +primary = yes +locale = en_US + +[alternatives.fr] +name = French +url_prefix = /fr/ +locale = fr +``` diff --git a/content/docs/project/structure/contents.lr b/content/docs/project/structure/contents.lr new file mode 100644 index 00000000..01d90777 --- /dev/null +++ b/content/docs/project/structure/contents.lr @@ -0,0 +1,103 @@ +title: Folder Structure +--- +summary: Explains the structure of a project on the filesystem. +--- +body: + +When you start your first Lektor project it's important to understand how data +is structured on the file system. A Lektor project is a folder on the file +system with a project file and a well defined structure. + +The most basic layout looks like this: + +``` +yourproject.lektorproject +content/ +models/ +templates/ +assets/ +``` + +There are a few more folders that play a role and plugins can add even more. + +## Project File + +The project file holds the main configuration of the project and is used to +identify the project for the user interface. The name does not matter but +it needs to have the `.lektorproject` extension. For more information about +that see [Project File :ref](../file/). + +## Folders + +### `content/` + +The content folder is named `content` and contains all the sources that are +built into the final project. Each folder within corresponds to a record +and the data of it is stored in the file by the name `contents.lr` within +which is a Lektor content file. All other files are considered attachments. + +Here a basic example layout to get an idea how this can look like: + +``` +content/ + contents.lr + projects/ + contents.lr + project-a/ + contents.lr + thumbnail.png + project-b/ + contents.lr + thumbnail.png + project-c/ + contents.lr + thumbnail.png +``` + +The content folder is explained in detail in [Content :ref](../content/). + +### `models/` + +This is the bread and butter of what makes Lektor powerful. The models define +exactly how the data from the content folder should be processed. The `models` +folder contains a list of INI files that each correspond to a single model. + +For more information about this refer to the [Data Modelling :ref](../models/) +documentation. + +### `templates/` + +Each model corresponds to a template from the `templates` folder. So if you have a +model named `page` the file named `page.html` from the templates folder will then +be used to render it. + +### `assets/` + +Whatever is stored in the `assets` folder is copied over verbatim to the final +result. So if you put a folder named `css` in there with all your static CSS +files they will appear as such in the final output. + +### `flowblocks/` + +The `flowblocks` folder contains models for blocks that are used by the +[Flow System :ref](../content/flow/). Flow blocks split part of a page into +smaller pieces that can be individually designed. They work similar to models +but are contained within a field of a model. + +### `packages/` + +For local plugin development the `packages/` folder can be used. Any plugin +stored in there is automatically actviated in the system. + +### `configs/` + +This folder (`configs/`) contains plugin specific config files. All the +files in there are named `.ini`. + +### `databags/` + +Lektor also supports [Data Bags :ref](../content/databags/) which go into a +folder named `databags/`. These are files with some general information +that can be accessed from templates. For instance you could store menus and +navigations there, API keys and much more. Just information you want to +access from different places and maybe not keep in templates directly. diff --git a/content/docs/quickstart/admin.png b/content/docs/quickstart/admin.png new file mode 100644 index 00000000..01afa7b2 Binary files /dev/null and b/content/docs/quickstart/admin.png differ diff --git a/content/docs/quickstart/contents.lr b/content/docs/quickstart/contents.lr new file mode 100644 index 00000000..cd4a3d1b --- /dev/null +++ b/content/docs/quickstart/contents.lr @@ -0,0 +1,98 @@ +title: Quickstart +--- +summary: Covers how to create a very basic website project with Lektor and run it. +--- +sort_key: 60 +--- +body: + +The best way to get started with Lektor is to use the `quickstart` command to +let Lektor generate a basic project layout for you. This requires the use of +the command line client which is the recommended way to do development until +the website is ready for end user management. + +If you do not have the `lektor` [command line executable :ref](../cli/) +available yet just consult the [Installation :ref](../installation) section of +the documentation. + +## Creating a New Project + +To create a new project open a terminal and navigate to the preferred location +of your project. Then execute the quickstart command to create a new project: + +``` +$ lektor quickstart +``` + +This will ask you a few questions and then create a new Lektor project with +some basic configuration for you. + +## Screencast + +If you want a video walkthrough you can have a look at the screencast which +explains the quickstart project a bit: + + + +## Running your Project + +Now that you have a project you can run it. As a developer the easiest way to +do that is to use the `server` which runs the project on your local machine +as if it was a dynamic website. + +All you have to do is to enter your project directory and run it: + +``` +$ cd yourproject +$ lektor server +``` + +This will automatically start the server and you can navigate to +[localhost:5000](http://localhost:5000/) go open the project. + +You can keep the server running, it will automatically rebuild your files as +they change. + +## Accessing the Admin + +While the development server is running you can use the built-in admin +interface. It can be accessed by clicking on the pencil symbol on a page +or by manually navigating to `/admin/`. + +screenshot of the admin + +## Building + +When you want to build the website for distribution you can make Lektor build +everything into static files. In fact, that's already happening in the +background while the development server is running. If you want to trigger +a build you can use the `build` command. By default it builds into a lektor +cache directory. + +``` +$ lektor build +``` + +You can also explicitly provide a path if you are not satisfied with the +default directory. To see where this directory is you can use the +`project-info` command: + +``` +$ lektor project-info --output-path +/Users/john/.../build-cache/6fdaeecab78d6aa99f86f586ab15da06 +``` + +All your generated files will end up in that folder for easy publishing. + +## Next Steps + +Now that you have done that, you might be interested in diving deeper into +Lektor. This might be good next steps: + +* The [Guides :ref](../guides/) which cover common setups. +* The [Deployment Documentation :ref](../deployment/) which shows how to + deploy a website to production. +* [Data Modelling :ref](../models/) for everything about how to model your + data. diff --git a/content/docs/templates/contents.lr b/content/docs/templates/contents.lr new file mode 100644 index 00000000..a52d84fc --- /dev/null +++ b/content/docs/templates/contents.lr @@ -0,0 +1,101 @@ +title: Templates +--- +sort_key: 90 +--- +summary: A quick introduction into templating in Lektor. +--- +body: + +Lektor uses the [Jinja2 :ext](http://jinja.pocoo.org/) templating language for +generating HTML out of your pages. You do not need to understand Jinja2 to +be able to generate beautiful websites but if you want to dive deep into the +powers of the templating language then you can learn more about it by +reading the [Jinja2 Documentation :ext](http://jinja.pocoo.org/docs). + +## Template Folder and Naming + +All templates are stored within the `templates/` folder. Templates typically +carry a `.html` extension. The default naming convention which is used in +Lektor is that the template name matches the model name. + +So if you have a model called `page` there would be a template named +`page.html`. Pages can however manually force a different template to be +rendered. + +## Template Context + +When a template is rendered it's rendered in the context of a few variables. +Which ones are available often depends on the situation the template is +evaluated in but the following are always available: + +| Variable | Description +| -------------- | -------------------------------------------------------- +| `this` | The current [Record :ref](../api/db/record/) that is being rendered. +| `site` | The database [Pad :ref](../api/db/pad/) that can be used to query the site. +| `alt` | A string that identifies the [Alternative :ref](../content/alts/) of the page. +| `config` | Gives access to the Lektor project configuration. + +## The First Template + +So let's dive in making our first template. In case you went through the +[Quickstart :ref](../quickstart/) which should give you an example model +to work with called `page`, otherwise just add one with the format shown +in the [Data Modelling Documentation :ref](../models/). + +With that we can create a page template named `templates/page.html`: + +```html+jinja +{% extends "layout.html" %} +{% block title %}{{ this.title }}{% endblock %} +{% block body %} +

{{ this.title }}

+ {{ this.body }} +{% endblock %} +``` + +If you are unfamiliar with Jinja this template might look very confusing, but +worry not. We will go through it step by step. + +* `{%` starts a Jinja section and `%}` ends it +* `extends` is a tag that instructs Jinja to extend another template. In + this case we extend our layout template. We will create this next. +* `block` creates or updates a block from the layout template. In this case + we have two blocks: one for the `title` of the page and another one for + the `body`. +* `{{` prints a variable and `}}` is the end of the print part. We do not + need to worry about escaping here as Jinja2 does that automatically for + us. + +## Layout Templates + +So we have this page template now, but what about this layout? Jinja2 +supports template inheritance where one template can inherit code from +another. In this case we configured our page template to inherit from +`layout.html`. Let's create it: + +```html+jinja + + +{% block title %}Welcome{% endblock %} — My Website + +
+

My Website

+ +
+
+ {% block body %}{% endblock %} +
+ +``` + +I hope you can see how the blocks work together now when template inheritance +is involved. + +## Everything about Templates + +Templates are the bread and butter of creating expressive websites with +Lektor. As such this is one of the most complex topics in the documentation +and split into smaller parts. Feel free to experiment around to see what +you can do with it. diff --git a/content/docs/templates/imageops/contents.lr b/content/docs/templates/imageops/contents.lr new file mode 100644 index 00000000..5fdd326f --- /dev/null +++ b/content/docs/templates/imageops/contents.lr @@ -0,0 +1,71 @@ +title: Image Operations +--- +summary: Shows how templates can work with images. +--- +sort_key: 30 +--- +body: + +Images are separate files and as such just embedded into HTML files. However +very often you want to perform modifications on these images that require +interacting with those files directly. This can be conveniently done directly +from the template code through the image APIs that Lektor provides. + +## Accessing Images + +To work with images one needs to get access to the image objects first. Images +are returned from the attachments of a page. If you want to make sure you +only operate with actual images the `.images` attribute can be used to filter +the query. For instance this iterates over all attached images of a page: + +```html+jinja +{% for image in this.attachments.images %} +
+{% endfor %} +``` + +## Accessing Image Data + +One of the more common operations is to access the direct image data. This +is resolution and file format. The `width`, `height` and `format` parameters +are provided for this. In some cases this is very useful to generate CSS code +that needs to know about the original resolution. For instance this achieves a +retina rendered background: + +```html+jinja +
+``` + +## Accessing EXIF Data + +Lektor can also give you access to a lot of the EXIF information that is stored +in the images. Not all EXIF information is available but the most common +values are. For the full list of attributes see [EXIF data +:ref](../../api/db/record/exif/). + +Here an example that shows the camera information: + +```html+jinja +
+ +

Camera: {{ image.exif.camera }} +

+``` + +## Generating Thumbnails + +While browsers are reasonably good at downscaling images themselves, you +still need to transmit the entire image. When you want smaller images it +often makes sense to generate thumbnails automatically. In Lektor each +image provides the [thumbnail :ref](../../../api/record/thumbnail/) method. + +It accepts the width and height of the target image. If the height is not +provided it will be scaled proportionally. The return value can be converted +into a URL with the `|url` filter: + +```html+jinja + +``` diff --git a/content/docs/templates/navigation/contents.lr b/content/docs/templates/navigation/contents.lr new file mode 100644 index 00000000..749c5759 --- /dev/null +++ b/content/docs/templates/navigation/contents.lr @@ -0,0 +1,84 @@ +title: Navigation +--- +summary: Shows how to create a dynamic navigation with Lektor. +--- +sort_key: 20 +--- +body: + +Websites are all about hyperlinks and being able to explore more. As such +it's important to be able to provide a good navigation experience for your +users. Templating in Lektor makes it very easy to make automatic navigation +that keeps up to date with your pages. Most of this involves generating +links with the help of the [URL Filter :ref](../urls/). + +## Basic Semi-Automatic Navigation + +The most basic form of navigation is a semi automatic one. It's one of the +most flexible ones while still being easy to maintain. It requires knowledge +of which pages to show and what the link title should be: + +```html+jinja + +``` + +In this case we use a list of pages (href and title) to automatically +generate some list items and we ask the current page (`this`) if it is +a child of the given path. Based on that information we automatically +add a class to the link. + +The index page requires a bit of special casing as we do not want it to +be active if any of it's children are active. So we just check if the +path of the current page is actually the path of the index page. + +## Fully Automatic Navigation + +Sometimes all we want is to show navigation links for all sub-pages of +a page. This is easy to accomplish as well: + +```html+jinja + +``` + +## Recursive Tree Navigation + +In some situations you want to show a tree like navigation. This is for +instance something that comes up when building site maps. In that situation +the recursive Jinja loop system comes really in. + +```html+jinja +
    + {% set root = site.get('/') %} + {% for child in root.children recursive %} + {{ child.title }} + {% if this.is_child_of(child) %} +
      {{ loop(child.children) }}
    + {% endif %} + {% endfor %} +
+``` + +This above template recursively renders out a part of the tree based +navigation around the current active page. For a concrete example for this: +this is how the navigation of this documentation is rendered. diff --git a/content/docs/templates/urls/contents.lr b/content/docs/templates/urls/contents.lr new file mode 100644 index 00000000..145ed3d6 --- /dev/null +++ b/content/docs/templates/urls/contents.lr @@ -0,0 +1,85 @@ +title: URLs and Links +--- +summary: Explains how to manage URLs and links in templates. +--- +sort_key: 10 +--- +body: + +Linking to other pages in templates is achieved through the `|url` and +`|asseturl` filters. These two filters will ensure that the links that are +generated are automatically relative to the current page so they will work +correctly anywhere where you host the page. They also deal with URL paths +that have been changed through configuration. + +## `url` Filter + +The [url :ref](../../api/templates/filters/url/) filter is the most useful +filter for URL generation and it comes in two flavors. It takes one optional +argument which is the `alt` if it should differ from the current one (see +[Alternatives :ref](../../content/alts/)). The filter can be applied to either +a string or a [Record :ref](../../api/db/record/) object. + +### Basic Navigation + +This is an example of how to just link to some specific pages that exist. +Because the path starts with a slash it will be treated as absolute path: + +```html+jinja + +``` + +### Relative Linking + +If you want to link relative to the current page, just leave out the +slash: + +```html+jinja +Go To Project A +``` + +### Link To Alternatives + +If you want to link to a page in a different alternative you can use the +optional `alt` parameter. For instance you can link to the current page +in another alternative (`.` indicates the current page): + +```html+jinja +Русский +``` + +### Link to Pages + +Because the URL filter can also accept entire record objects you can easily +link to all children of a page: + +```html+jinja + +``` + +## `asseturl` Filter + +A second filter that is available is the [asseturl +:ref](../../api/templates/filters/asseturl/) filter. It works similar to +`|url` but can only link to assets from the `assets/` folder. However unlike +`|url` it will append a dummy query string with a hash of the source asset. +This ensures that when the asset changes it will be newly cached. + +Example: + +```html+jinja + +``` + +The end result will look something like this: + +```html + +``` diff --git a/content/docs/what/admin.png b/content/docs/what/admin.png new file mode 100644 index 00000000..e33aa436 Binary files /dev/null and b/content/docs/what/admin.png differ diff --git a/content/docs/what/contents.lr b/content/docs/what/contents.lr new file mode 100644 index 00000000..596400e1 --- /dev/null +++ b/content/docs/what/contents.lr @@ -0,0 +1,80 @@ +title: What is Lektor +--- +summary: A brief introduction into what makes Lektor special. +--- +sort_key: 20 +--- +body: + +When it comes to creating websites, there ludicrous amount of tools available. +They range from full blown content management solutions like Drupal over +desktop solutions like Google Web Designer to Cloud Hosted Website solutions +like WIX to more programmer focused approaches like Jekyll which generate +websites out of templates and markdown files. + +*Lektor is different than any of these.* + +## Lektor is Static + +Lektor learned from the huge range of static file generators like Jekyll, +Pelican, Hugo, Middleman and many more about the values of generating a +completely static website. This means that unlike WordPress or similar +solutions it does not run on a server, but your local computer (or a build +server) and generates out static HTML that can be uploaded to any web server +or content distribution platform like S3 with CloudFront. + +Why go static? Because the vast, vast majority of websites will be read many +more times than they will update. This is crucial because dynamic does not +come for free. It needs server resources and because program code is running +there it needs to be kept up to date for to ensure there are no security +problems that are left unpatched. Also when a website gets a sudden spike +of traffic a static website will stay up for longer on the same server than +a dynamic one that needs to execute code. + +Sure, there are some things you cannot do on a static website, but those +things you would not use Lektor for. For small dynamic sections, JavaScript +and pairing up with other services is a good solution. + + + +## Lektor is a CMS + +However Lektor also takes from content management systems like WordPress +and provides a flexible browser based admin interface from which you can +edit your website's contents. Unlike traditional CMS solutions however it +runs entirely on your own computer. + +This means you can give a Lektor website to people that have no understanding +of programming and they can still modify the content and update the website. + + + +## Lektor is a Framework + +Lastly however Lektor learns from experience in writing web frameworks. Lektor +is much more than a website generator because it is based on a very flexible +internal flat file database which can be used to model any website content. +Unlike static blog generators which are based on some markdown content and +“front matter” metadata Lektor's content is 100% configurable. + +If you have ever used a web framework like Django or Ruby on Rails you might +feel right at home in how you can model and query your data. + + + +## Collaborate and Synchronize + +Lektor acknowledges that there are web developers and content editors and that +their interests and preferences are very different. This is reflected heavily +in the design of Lektor and if you make your first Lektor project you can see +why. A web developer would go in and setup the theme and structure of a +Lektor project and content creators can then fill in the content of the site. + +The collaboration can be based on version control systems like git or just +basic solutions like Dropbox. It's intentionally built so that collaboration +can work via the most basic systems like Dropbox or just network shares. + +When you go live, you can synchronize up your changes into a remote server +just as easily. + + diff --git a/content/docs/what/git.png b/content/docs/what/git.png new file mode 100644 index 00000000..a6628aa6 Binary files /dev/null and b/content/docs/what/git.png differ diff --git a/content/docs/what/static.png b/content/docs/what/static.png new file mode 100644 index 00000000..c03cae21 Binary files /dev/null and b/content/docs/what/static.png differ diff --git a/content/docs/what/templates.png b/content/docs/what/templates.png new file mode 100644 index 00000000..96ebdfae Binary files /dev/null and b/content/docs/what/templates.png differ diff --git a/content/downloads/contents.lr b/content/downloads/contents.lr new file mode 100644 index 00000000..6f5e434f --- /dev/null +++ b/content/downloads/contents.lr @@ -0,0 +1,46 @@ +_model: page +--- +title: Downloads +--- +body: + +#### banner #### +image: header.jpg +#### text-block #### +class: default +---- +text: + +Lektor is an [Open Source Project](../license/) and freely available to +download. As it's currently still under heavy development not all platforms +are equally well supported. + +## Command Line Interface + +If you are on Linux or Mac you can install the command line version of +Lektor by copy/pasting a command into your terminal: + +``` +curl -sf https://www.getlektor.com/install.sh | sh +``` + +The command line application is written in Python and the current releases +can be found on PyPI: [pypi/Lektor](http://pypi.python.org/pypi/Lektor). + +If you know Python, you can also pip install it: + +``` +pip install Lektor +``` + +This will install Lektor for you. You might have to run it with `sudo` if +your current user does not have rights to write into `/usr/local`. + +## Desktop Application + +If you are on OS X you are in luck because you can use an installable +version of Lektor that comes with a graphical user interface. It also +includes the command line utilities. + + + Download Desktop App diff --git a/content/downloads/header.jpg b/content/downloads/header.jpg new file mode 100644 index 00000000..3866618b Binary files /dev/null and b/content/downloads/header.jpg differ diff --git a/content/edit-post.png b/content/edit-post.png new file mode 100644 index 00000000..cf4361e0 Binary files /dev/null and b/content/edit-post.png differ diff --git a/content/filesystem.png b/content/filesystem.png new file mode 100644 index 00000000..9b0d1312 Binary files /dev/null and b/content/filesystem.png differ diff --git a/content/header.jpg b/content/header.jpg new file mode 100644 index 00000000..a54e1cff Binary files /dev/null and b/content/header.jpg differ diff --git a/content/license/contents.lr b/content/license/contents.lr new file mode 100644 index 00000000..0c1cd60b --- /dev/null +++ b/content/license/contents.lr @@ -0,0 +1,44 @@ +_model: page +--- +title: License +--- +body: + +#### banner #### +image: header.jpg +#### text-block #### +class: default +---- +text: + +Copyright (c) 2015 by the Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/content/license/header.jpg b/content/license/header.jpg new file mode 100644 index 00000000..09fd6b09 Binary files /dev/null and b/content/license/header.jpg differ diff --git a/content/logo.png b/content/logo.png new file mode 100644 index 00000000..e64c17ab Binary files /dev/null and b/content/logo.png differ diff --git a/content/preview.png b/content/preview.png new file mode 100644 index 00000000..9c0c1609 Binary files /dev/null and b/content/preview.png differ diff --git a/content/publish.png b/content/publish.png new file mode 100644 index 00000000..34f214b5 Binary files /dev/null and b/content/publish.png differ diff --git a/content/sitemap.xml/contents.lr b/content/sitemap.xml/contents.lr new file mode 100644 index 00000000..8b1bce38 --- /dev/null +++ b/content/sitemap.xml/contents.lr @@ -0,0 +1 @@ +_template: sitemap.xml diff --git a/content/structure.png b/content/structure.png new file mode 100644 index 00000000..2bff5183 Binary files /dev/null and b/content/structure.png differ diff --git a/databags/menu.ini b/databags/menu.ini new file mode 100644 index 00000000..7a51428f --- /dev/null +++ b/databags/menu.ini @@ -0,0 +1,11 @@ +[download] +path = /downloads +label = Download + +[docs] +path = /docs +label = Documentation + +[blog] +path = /blog +label = Blog diff --git a/flowblocks/banner.ini b/flowblocks/banner.ini new file mode 100644 index 00000000..d88eceb8 --- /dev/null +++ b/flowblocks/banner.ini @@ -0,0 +1,28 @@ +[block] +name = Banner +button_label = [[flag]] + +[fields.image] +label = Image +type = select +source = record.attachments.images +width = 2/4 + +[fields.height] +label = Height +type = select +choices = half, full, 500, 300 +choice_labels = Half, Full, 500px, 300px +default = 300 +width = 1/4 + +[fields.class] +label = Class +type = select +choices = dark +choice_labels = Dark +width = 1/4 + +[fields.contents] +label = Contents +type = html diff --git a/flowblocks/slide.ini b/flowblocks/slide.ini new file mode 100644 index 00000000..5e9957a0 --- /dev/null +++ b/flowblocks/slide.ini @@ -0,0 +1,11 @@ +[block] +name = Slide + +[fields.description] +label = Description +type = markdown + +[fields.image] +label = Background Image +type = select +source = record.attachments.images diff --git a/flowblocks/slideshow.ini b/flowblocks/slideshow.ini new file mode 100644 index 00000000..ea0fc9fe --- /dev/null +++ b/flowblocks/slideshow.ini @@ -0,0 +1,8 @@ +[block] +name = Slideshow +button_label = [[list-alt]] + +[fields.slides] +label = Slides +type = flow +flow_blocks = slide diff --git a/flowblocks/text-block.ini b/flowblocks/text-block.ini new file mode 100644 index 00000000..7b541995 --- /dev/null +++ b/flowblocks/text-block.ini @@ -0,0 +1,14 @@ +[block] +name = Text Block +button_label = [[paragraph]] + +[fields.text] +label = Text +type = markdown + +[fields.class] +label = Class +type = select +choices = default, centered, two-column-list +choice_labels = Default, Centered, Two Column List +default = default diff --git a/models/blog-post.ini b/models/blog-post.ini new file mode 100644 index 00000000..a3d72005 --- /dev/null +++ b/models/blog-post.ini @@ -0,0 +1,34 @@ +[model] +name = Blog Post +label = {{ this.title }} +hidden = yes + +[fields.title] +label = Title +type = string +size = large + +[fields.author] +label = Author +type = string +width = 1/2 + +[fields.twitter_handle] +label = Twitter Handle +type = string +width = 1/4 +addon_label = @ + +[fields.pub_date] +label = Publication date +type = date +width = 1/4 + +[fields.summary] +label = Summary +type = text + +[fields.body] +label = Body +type = flow +flow_blocks = banner, text-block diff --git a/models/blog.ini b/models/blog.ini new file mode 100644 index 00000000..0786de95 --- /dev/null +++ b/models/blog.ini @@ -0,0 +1,13 @@ +[model] +name = Blog +label = Blog +hidden = yes + +[children] +model = blog-post +order_by = -pub_date, title +slug_format = {{ ((this.pub_date.year ~ '/' ~ this.pub_date.month ~ '/') if this.pub_date) ~ this._id }} + +[pagination] +enabled = yes +per_page = 10 diff --git a/models/doc-page.ini b/models/doc-page.ini new file mode 100644 index 00000000..73e387a4 --- /dev/null +++ b/models/doc-page.ini @@ -0,0 +1,59 @@ +[model] +name = Documentation Page +label = {{ this.title }} + +[children] +model = doc-page +order_by = sort_key, title + +[fields.title] +label = Title +type = string +size = large + +[fields.summary] +label = Summary +type = string +width = 1/2 + +[fields.sort_key] +label = Sort key +type = sort_key +width = 1/4 + +[fields.allow_comments] +label = Allow Comments +type = boolean +default = yes +checkbox_label = Show comment box +width = 1/4 + +[fields.body] +label = Body +type = markdown + +[fields.type] +label = Technical Type +type = select +description = Of what general type this doc page is. If not set, it's a normal doc page. +choices = class, function, method, property, operator, filter, cmdlet, event, type +choice_labels = Class, Function, Method, Property, Operator, Filter, Commandlet, Event, Field Type +width = 1/2 + +[fields.module] +label = Module +type = string +description = Identifies the module a class or function is contained in. +choices = class, function, method +choice_labels = Class, Function, Method +width = 1/2 + +[fields.signature] +label = Signature +type = string +description = An optional signature for a type. + +[fields.template_var] +label = Template Variable +type = string +description = An optional template variable if it exists there as such. diff --git a/models/page.ini b/models/page.ini new file mode 100644 index 00000000..0d7a0c50 --- /dev/null +++ b/models/page.ini @@ -0,0 +1,13 @@ +[model] +name = Page +label = {{ this.title }} + +[fields.title] +label = Title +type = string +size = large + +[fields.body] +label = Body +type = flow +flow_blocks = text-block, banner diff --git a/models/poster.ini b/models/poster.ini new file mode 100644 index 00000000..6aff5f1b --- /dev/null +++ b/models/poster.ini @@ -0,0 +1,13 @@ +[model] +name = Poster +label = {{ this.title }} + +[fields.title] +label = Title +type = string +size = large + +[fields.body] +label = Body +type = flow +flow_blocks = slideshow, text-block, banner diff --git a/packages/atom-feed-support/.gitignore b/packages/atom-feed-support/.gitignore new file mode 100644 index 00000000..463960b8 --- /dev/null +++ b/packages/atom-feed-support/.gitignore @@ -0,0 +1,5 @@ +dist +build +*.pyc +*.pyo +*.egg-info diff --git a/packages/atom-feed-support/lektor_atom_feed_support.py b/packages/atom-feed-support/lektor_atom_feed_support.py new file mode 100644 index 00000000..87daeab3 --- /dev/null +++ b/packages/atom-feed-support/lektor_atom_feed_support.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +import posixpath +import hashlib +import uuid +from datetime import datetime, date +from lektor.pluginsystem import Plugin +from lektor.context import get_ctx, url_to +from lektor.utils import get_structure_hash + +from werkzeug.contrib.atom import AtomFeed +from markupsafe import escape + + +def get_id(input): + return uuid.UUID(bytes=hashlib.md5(input).digest(), + version=3).urn + + +def get_item_title(item, field): + if field in item: + return item[field] + return item.record_label + + +def get_item_body(item, field): + if field not in item: + raise RuntimeError('Body field not found: %r' % field) + with get_ctx().changed_base_url(item.url_path): + return unicode(escape(item[field])) + + +def get_item_author(item, field): + if field in item: + return unicode(item[field]) + return 'Unknown' + + +def get_item_updated(item, field): + if field in item: + rv = item[field] + else: + rv = datetime.utcnow() + if isinstance(rv, date): + rv = datetime(*rv.timetuple()[:3]) + return rv + + +def create_atom_feed(filename='atom.xml', + title=None, subtitle=None, + link=None, items=None, item_title_field='title', + item_body_field='body', item_author_field='author', + item_date_field='pub_date'): + ctx = get_ctx() + if ctx is None: + raise RuntimeError('A context is required') + source = ctx.source + if source is None: + raise RuntimeError('Can only generate feeds out of sources.') + if items is None: + raise RuntimeError('An item expression is required') + + artifact_name = posixpath.join(source.url_path, filename) + config_hash = get_structure_hash({ + 'filename': filename, + 'title': title, + 'subtitle': subtitle, + 'link': link, + 'items': items, + 'item_title_field': item_title_field, + 'item_body_field': item_body_field, + 'item_author_field': item_author_field, + 'item_date_field': item_date_field, + }) + + # Iterating over the items will resolve dependencies. As we are very + # interested in those, we need to capture tem, + dependencies = set() + with ctx.gather_dependencies(dependencies.add): + items = list(items) + + here = posixpath.join(source.url_path, filename) + feed_url = url_to(here, external=True) + embed_url = url_to(source, external=True) + + @ctx.sub_artifact(artifact_name, source_obj=ctx.source, + sources=list(dependencies), + config_hash=config_hash) + def generate_feed(artifact): + feed = AtomFeed( + title=title or 'Feed', + subtitle=unicode(subtitle or ''), + subtitle_type=hasattr(subtitle, '__html__') and 'html' or 'text', + feed_url=feed_url, + url=embed_url, + id=get_id(ctx.env.project.id + 'lektor') + ) + + for item in items: + feed.add( + get_item_title(item, item_title_field), + get_item_body(item, item_body_field), + xml_base=url_to(item, external=True), + url=url_to(item, external=True), + content_type='html', + id=get_id(u'%slektor/%s' % ( + ctx.env.project.id, + item['_path'].encode('utf-8'), + )), + author=get_item_author(item, item_author_field), + updated=get_item_updated(item, item_date_field)) + + with artifact.open('wb') as f: + f.write(feed.to_string().encode('utf-8') + '\n') + + return artifact_name + + +class AtomFeedSupportPlugin(Plugin): + name = u'Atom Feed Support' + description = u'Adds basic Atom feed support to Lektor.' + + def on_setup_env(self, **extra): + self.env.jinja_env.globals['create_atom_feed'] = create_atom_feed diff --git a/packages/atom-feed-support/setup.py b/packages/atom-feed-support/setup.py new file mode 100644 index 00000000..975a9351 --- /dev/null +++ b/packages/atom-feed-support/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +setup( + name='lektor-atom-feed-support', + version='0.1', + author=u'Armin Ronacher', + author_email='armin.ronacher@active-4.com', + license='MIT', + py_modules=['lektor_atom_feed_support'], + entry_points={ + 'lektor.plugins': [ + 'atom-feed-support = lektor_atom_feed_support:AtomFeedSupportPlugin', + ] + } +) diff --git a/packages/blog-archive/.gitignore b/packages/blog-archive/.gitignore new file mode 100644 index 00000000..463960b8 --- /dev/null +++ b/packages/blog-archive/.gitignore @@ -0,0 +1,5 @@ +dist +build +*.pyc +*.pyo +*.egg-info diff --git a/packages/blog-archive/lektor_blog_archive.py b/packages/blog-archive/lektor_blog_archive.py new file mode 100644 index 00000000..38c06d23 --- /dev/null +++ b/packages/blog-archive/lektor_blog_archive.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +import posixpath +from datetime import date + +from werkzeug.utils import cached_property + +from lektor.pluginsystem import Plugin +from lektor.sourceobj import VirtualSourceObject +from lektor.build_programs import BuildProgram +from lektor.context import get_ctx + + +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): + + def __init__(self, parent, plugin, items=None, year=None, month=None): + VirtualSourceObject.__init__(self, parent) + self.plugin = plugin + self._items = items + self.year = year + self.month = month + + @property + def date(self): + if self.year is None: + raise AttributeError() + return date(self.year, self.month or 1, 1) + + @property + def year_archive(self): + if self.year is None: + 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 + def year_archives(self): + if self.year is not None: + return [] + years = set() + for item in self.parent.children: + pub_date = self.plugin.get_pub_date(item) + if pub_date: + years.add(pub_date.year) + return [BlogArchive(self.parent, self.plugin, + year=year) for year in sorted(years)] + + @property + def items(self): + if self.year is None: + return [] + if self._items is not None: + return self._items + rv = list(self._iter_items()) + self._items = rv + return rv + + def _iter_items(self): + for item in self.parent.children: + 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 + def has_any_items(self): + if self._items is not None: + return bool(self._items) + for item in self._iter_items(): + return True + return False + + @property + def items_by_months(self): + months = {} + for item in self.items: + pub_date = self.plugin.get_pub_date(item) + months.setdefault(date(pub_date.year, pub_date.month, 1), + []).append(item) + return [(BlogArchive(self.parent, self.plugin, + year=d.year, month=d.month), i) + for d, i in sorted(months.items())] + + @property + def url_path(self): + 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 + def template_name(self): + if self.year is None: + return 'blog-archive/index.html' + if self.month is None: + return 'blog-archive/year.html' + return 'blog-archive/month.html' + + +class BlogArchiveBuildProgram(BuildProgram): + + def produce_artifacts(self): + self.declare_artifact( + posixpath.join(self.source.url_path, 'index.html'), + sources=list(self.source.iter_source_filenames())) + + def build_artifact(self, artifact): + artifact.render_template_into(self.source.template_name, + this=self.source) + + +class BlogArchivePlugin(Plugin): + name = u'Blog Archive' + description = u'Adds archives to a blog.' + + def get_pub_date(self, post): + key = self.get_config().get('pub_date_field', 'pub_date') + return post[key] + + def get_blog_path(self): + return self.get_config().get('blog_path', '/blog') + + def get_archive_index_path(self): + return self.get_config().get('archive_path', 'archive').strip('/') + + 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): + blog_path = self.get_blog_path() + self.env.add_build_program(BlogArchive, BlogArchiveBuildProgram) + + def get_blog_archive(): + pad = get_ctx().pad + blog = pad.get(blog_path) + if blog is not None: + return BlogArchive(blog, self) + self.env.jinja_env.globals['get_blog_archive'] = get_blog_archive + + @self.env.urlresolver + def archive_resolver(node, url_path): + if node.path != blog_path: + return + + archive_index = get_path_segments(self.get_archive_index_path()) + if url_path == archive_index: + return BlogArchive(node, self) + + year_prefix = get_path_segments(self.get_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 \ + url_path[len(year_prefix)].isdigit() and \ + len(url_path) == len(year_prefix) + 1: + year = int(url_path[len(year_prefix)]) + elif (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)]) + month = int(url_path[len(month_prefix) + 1]) + else: + return None + + rv = BlogArchive(node, self, year=year, month=month) + if rv.has_any_items: + return rv + + @self.env.generator + def genererate_blog_archive_pages(source): + if source.path != blog_path: + return + + blog = source + + years = {} + months = {} + for post in blog.children: + pub_date = self.get_pub_date(post) + if pub_date: + years.setdefault(pub_date.year, []).append(post) + months.setdefault((pub_date.year, + pub_date.month), []).append(post) + + yield BlogArchive(blog, self) + for year, items in sorted(years.items()): + yield BlogArchive(blog, self, year=year, items=items) + for (year, month), items in sorted(months.items()): + yield BlogArchive(blog, self, year=year, month=month, + items=items) diff --git a/packages/blog-archive/setup.py b/packages/blog-archive/setup.py new file mode 100644 index 00000000..4913d8bf --- /dev/null +++ b/packages/blog-archive/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +setup( + name='lektor-blog-archive', + version='0.1', + author=u'Armin Ronacher', + author_email='armin.ronacher@active-4.com', + license='MIT', + py_modules=['lektor_blog_archive'], + entry_points={ + 'lektor.plugins': [ + 'blog-archive = lektor_blog_archive:BlogArchivePlugin', + ] + } +) diff --git a/packages/markdown-header-anchors/lektor_markdown_header_anchors.py b/packages/markdown-header-anchors/lektor_markdown_header_anchors.py new file mode 100644 index 00000000..12b439a1 --- /dev/null +++ b/packages/markdown-header-anchors/lektor_markdown_header_anchors.py @@ -0,0 +1,42 @@ +from lektor.pluginsystem import Plugin +from lektor.utils import slugify +from markupsafe import Markup +from collections import namedtuple + + +TocEntry = namedtuple('TocEntry', ['anchor', 'title', 'children']) + + +class MarkdownHeaderAnchorsPlugin(Plugin): + name = 'Markdown Header Anchors' + description = 'Adds anchors to markdown headers.' + + def on_markdown_config(self, config, **extra): + class HeaderAnchorMixin(object): + def header(renderer, text, level, raw): + anchor = slugify(raw) + renderer.meta['toc'].append((level, anchor, Markup(text))) + return '%s' % (level, anchor, text, level) + config.renderer_mixins.append(HeaderAnchorMixin) + + def on_markdown_meta_init(self, meta, **extra): + meta['toc'] = [] + + def on_markdown_meta_postprocess(self, meta, **extra): + prev_level = None + toc = [] + stack = [toc] + + for level, anchor, title in meta['toc']: + if prev_level is None: + prev_level = level + elif prev_level == level - 1: + stack.append(stack[-1][-1][2]) + prev_level = level + elif prev_level > level: + while prev_level > level: + stack.pop() + prev_level -= 1 + stack[-1].append(TocEntry(anchor, title, [])) + + meta['toc'] = toc diff --git a/packages/markdown-header-anchors/setup.py b/packages/markdown-header-anchors/setup.py new file mode 100644 index 00000000..ff52308e --- /dev/null +++ b/packages/markdown-header-anchors/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='lektor-markdown-header-anchors', + version='0.1', + py_modules=['lektor_markdown_header-anchors'], + entry_points={ + 'lektor.plugins': [ + 'markdown-header-anchors = lektor_markdown_header_anchors:MarkdownHeaderAnchorsPlugin', + ] + } +) diff --git a/packages/markdown-highlighter/lektor_markdown_highlighter.py b/packages/markdown-highlighter/lektor_markdown_highlighter.py new file mode 100644 index 00000000..ade04415 --- /dev/null +++ b/packages/markdown-highlighter/lektor_markdown_highlighter.py @@ -0,0 +1,49 @@ +from lektor.pluginsystem import Plugin +from lektor.context import get_ctx + +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_by_name + +from markupsafe import Markup + + +class MarkdownHighlighterPlugin(Plugin): + name = 'Markdown Highlighter' + description = 'Adds syntax highlighting for markdown blocks.' + + def get_formatter(self): + return HtmlFormatter(style=self.get_style()) + + def get_style(self): + return self.get_config().get('pygments.style', 'default') + + def highlight_code(self, text, lang): + get_ctx().record_dependency(self.config_filename) + lexer = get_lexer_by_name(lang) + return highlight(text, lexer, self.get_formatter()) + + def on_markdown_config(self, config, **extra): + class HighlightMixin(object): + def block_code(ren, text, lang): + if not lang: + return super(HighlightMixin, ren).block_code(text, lang) + return self.highlight_code(text, lang) + config.renderer_mixins.append(HighlightMixin) + + def on_setup_env(self, **extra): + def get_pygments_stylesheet(artifact_name='/static/pygments.css'): + ctx = get_ctx() + @ctx.sub_artifact(artifact_name=artifact_name, sources=[ + self.config_filename]) + def build_stylesheet(artifact): + with artifact.open('w') as f: + f.write(self.get_formatter().get_style_defs()) + return artifact_name + + def pygmentize(text, lang): + return Markup(self.highlight_code(text, lang)) + + self.env.jinja_env.globals['get_pygments_stylesheet'] = \ + get_pygments_stylesheet + self.env.jinja_env.filters['pygmentize'] = pygmentize diff --git a/packages/markdown-highlighter/setup.py b/packages/markdown-highlighter/setup.py new file mode 100644 index 00000000..c02d3a88 --- /dev/null +++ b/packages/markdown-highlighter/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +setup( + name='lektor-markdown-highlighter', + version='0.1', + py_modules=['lektor_markdown_highlighter'], + entry_points={ + 'lektor.plugins': [ + 'markdown-highlighter = lektor_markdown_highlighter:MarkdownHighlighterPlugin', + ] + }, + install_requires=[ + 'Pygments', + ] +) diff --git a/packages/markdown-link-classes/lektor_markdown_link_classes.py b/packages/markdown-link-classes/lektor_markdown_link_classes.py new file mode 100644 index 00000000..34354e44 --- /dev/null +++ b/packages/markdown-link-classes/lektor_markdown_link_classes.py @@ -0,0 +1,35 @@ +import re +from lektor.pluginsystem import Plugin +from markupsafe import escape + + +_class_re = re.compile(r'\s+:([a-zA-Z0-9_-]+)') + + +def split_classes(text): + classes = [] + def _handle_match(match): + classes.append(match.group(1)) + return '' + text = _class_re.sub(_handle_match, text).replace('\\:', ':') + return text, classes + + +class MarkdownLinkClassesPlugin(Plugin): + name = 'Markdown Link Classes' + description = 'Adds the ability to add classes to links.' + + def on_markdown_config(self, config, **extra): + class LinkClassesMixin(object): + def link(renderer, link, title, text): + text, classes = split_classes(text) + if link.startswith('javascript:'): + link = '' + attr = ['href="%s"' % escape(link)] + if title: + attr.append('title="%s"' % escape(title)) + if classes: + attr.append('class="%s"' % ' '.join( + escape(x) for x in classes)) + return '%s' % (' '.join(attr), text) + config.renderer_mixins.append(LinkClassesMixin) diff --git a/packages/markdown-link-classes/setup.py b/packages/markdown-link-classes/setup.py new file mode 100644 index 00000000..fe680405 --- /dev/null +++ b/packages/markdown-link-classes/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup( + name='lektor-markdown-link-classes', + version='0.1', + author='Armin Ronacher', + author_email='armin.ronacher@active-4.com', + license='MIT', + py_modules=['lektor_markdown_link_classes'], + url='http://github.com/lektor/lektor', + entry_points={ + 'lektor.plugins': [ + 'markdown-link-classes = lektor_markdown_link_classes:MarkdownLinkClassesPlugin', + ] + } +) diff --git a/scripts/dump-cli-help.py b/scripts/dump-cli-help.py new file mode 100644 index 00000000..139e6124 --- /dev/null +++ b/scripts/dump-cli-help.py @@ -0,0 +1,98 @@ +import os +import click +from click.formatting import join_options + +from lektor.cli import cli as root_command + + +OUT = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', 'content', 'docs', 'cli')) + + +def get_opts(param): + any_prefix_is_slash = [] + def _write(opts): + rv, any_slashes = join_options(opts) + if any_slashes: + any_prefix_is_slash[:] = [True] + if not param.is_flag and not param.count: + rv += ' ' + param.make_metavar() + return rv + rv = [_write(param.opts)] + if param.secondary_opts: + rv.append(_write(param.secondary_opts)) + return (any_prefix_is_slash and '; ' or ' / ').join(rv) + + +def write_page(data): + path = data['path'][1:] + if not path: + return + filename = os.path.join(OUT, *(path + ['contents.lr'])) + dirname = os.path.dirname(filename) + try: + os.makedirs(dirname) + except OSError: + pass + + args = [x['metavar'] for x in data['arguments']] + body = [ + '`%s`' % ' '.join(data['path'] + args), + '', + data['help'] or '', + ] + + body.append('') + body.append('## Options') + body.append('') + for opt in data['options']: + prefix = '- `%s`: ' % opt['opt_string'] + for line in click.wrap_text( + opt['help'] or '', 74, prefix, ' ').splitlines(): + body.append(line) + body.append('- `--help`: print this help page.') + + fields = [ + ('comment', 'This file is auto generated by dump-cli-help.py'), + ('title', path[-1]), + ('summary', data['summary']), + ('type', 'cmdlet'), + ('body', '\n'.join(body)), + ] + + with open(filename, 'w') as f: + f.write('\n---\n'.join('%s:%s%s' % ( + k, + len(v.splitlines()) > 1 and '\n\n' or ' ', + v + ) for k, v in fields)) + + +def dump_command(cmd, path): + data = { + 'path': path, + 'summary': cmd.short_help, + 'help': cmd.help.replace('\b', ''), + 'options': [], + 'arguments': [], + } + + for param in cmd.params: + if isinstance(param, click.Option): + data['options'].append({ + 'opt_string': get_opts(param), + 'help': param.help, + }) + else: + data['arguments'].append({ + 'metavar': param.make_metavar(), + }) + + write_page(data) + + if isinstance(cmd, click.Group): + for child_name, child_cmd in cmd.commands.iteritems(): + dump_command(child_cmd, path + [child_name]) + + +dump_command(root_command, ['lektor']) diff --git a/templates/blocks/banner.html b/templates/blocks/banner.html new file mode 100644 index 00000000..b7299c49 --- /dev/null +++ b/templates/blocks/banner.html @@ -0,0 +1,8 @@ +{% set image = record.attachments.get(this.image) %} + diff --git a/templates/blocks/slideshow.html b/templates/blocks/slideshow.html new file mode 100644 index 00000000..bfde2a77 --- /dev/null +++ b/templates/blocks/slideshow.html @@ -0,0 +1,37 @@ +{% set carousel_id = 'carousel-' ~ get_random_id() %} +
+
+
+ +
+
+
diff --git a/templates/blocks/text-block.html b/templates/blocks/text-block.html new file mode 100644 index 00000000..13979674 --- /dev/null +++ b/templates/blocks/text-block.html @@ -0,0 +1,3 @@ +
+ {{ this.text }} +
diff --git a/templates/blog-archive/index.html b/templates/blog-archive/index.html new file mode 100644 index 00000000..028b8cb8 --- /dev/null +++ b/templates/blog-archive/index.html @@ -0,0 +1,14 @@ +{% extends "blog_layout.html" %} +{% block title %}Archive | The Transcript{% endblock %} +{% block blog_body %} +

The Transcript Archive

+

+ « back to the blog +

+ There have been posts in the following years: +

+{% endblock %} diff --git a/templates/blog-archive/month.html b/templates/blog-archive/month.html new file mode 100644 index 00000000..c014a382 --- /dev/null +++ b/templates/blog-archive/month.html @@ -0,0 +1,12 @@ +{% extends "blog_layout.html" %} +{% block title %}{{ this.date|dateformat('MMMM yyyy') }} Archive | The Transcript{% endblock %} +{% block blog_body %} +

The Transcript, {{ this.date|dateformat('MMMM yyyy') }}

+

+ « Back to {{ this.year }} +

    + {% for post in this.items %} +
  • {{ post.title }} by {{ post.author }} on {{ post.pub_date|dateformat }} + {% endfor %} +
+{% endblock %} diff --git a/templates/blog-archive/year.html b/templates/blog-archive/year.html new file mode 100644 index 00000000..a194f968 --- /dev/null +++ b/templates/blog-archive/year.html @@ -0,0 +1,18 @@ +{% extends "blog_layout.html" %} +{% block title %}{{ this.year }} Archive | The Transcript{% endblock %} +{% block blog_body %} +

The Transcript in {{ this.year }}

+

+ « Back to the archive +

+{% endblock %} diff --git a/templates/blog-post.html b/templates/blog-post.html new file mode 100644 index 00000000..8497b6b6 --- /dev/null +++ b/templates/blog-post.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} +{% block title %}{{ this.title }} | The Transcript{% endblock %} +{% from 'macros/blocksupport.html' import render_full_width_blocks %} +{% block outerbody %} +
+ {% call render_full_width_blocks(this.body.blocks) %} +
+

{{ this.title }}

+

+ by + {% if this.twitter_handle %} + {{ + this.author or this.twitter_handle }} + {% else %} + {{ this.author }} + {% endif %} + on {{ this.pub_date|dateformat('full') }} +

+
+ {% endcall %} +
+{% endblock %} diff --git a/templates/blog.html b/templates/blog.html new file mode 100644 index 00000000..ab636996 --- /dev/null +++ b/templates/blog.html @@ -0,0 +1,28 @@ +{% extends "blog_layout.html" %} +{% from "macros/pagination.html" import render_pagination %} +{% block title %}The Transcript{% endblock %} + {% block blog_body %} +

The Transcript

+ {% for post in this.pagination.items %} +
+

{{ post.title }}

+

+ {{ post.summary }} + Read more … +

+ by + {% if post.twitter_handle %} + {{ + post.author or post.twitter_handle }} + {% else %} + {{ post.author }} + {% endif %} + on {{ post.pub_date|dateformat }} +

+
+ {% endfor %} + + {% if this.pagination.pages > 1 %} + {{ render_pagination(this.pagination) }} + {% endif %} +{% endblock %} diff --git a/templates/blog_layout.html b/templates/blog_layout.html new file mode 100644 index 00000000..4d313ee8 --- /dev/null +++ b/templates/blog_layout.html @@ -0,0 +1,41 @@ +{% extends "layout.html" %} +{% block title %}The Transcript{% endblock %} +{% block outerbody %} + {% set image = site.get('/blog').attachments.images.first() %} + {% if image %} + + {% endif %} + {{ super() }} +{% endblock %} +{% set feed_url = create_atom_feed( + filename='/blog/feed.xml', + title='The Transcript', + subtitle='Lektor\'s blog', + link='/blog'|url, + items=site.query('/blog').limit(10) +) %} +{% block body %} +
+
+ {% block blog_body %}{% endblock %} +
+
+

About This Blog

+

+ “The Transcript” is the blog about Lektor, a new solution in static content managment. +

+ Subscribe for the latest news about Lektor as well as content + management and web development. +

+

Missed a Post?

+ +
+
+{% endblock %} diff --git a/templates/doc-page.html b/templates/doc-page.html new file mode 100644 index 00000000..3ad63996 --- /dev/null +++ b/templates/doc-page.html @@ -0,0 +1,79 @@ +{% extends "layout.html" %} +{% from "macros/docs.html" import get_doc_icon, get_doc_link, get_doc_header %} +{% block title %}{{ (this.title ~ ' | ') if this.title != 'Documentation' }}Documentation{% endblock %} +{% block body %} +
+
+
    + Welcome + {% set docs = site.get('/docs') %} + {% for child in docs.children recursive %} + {{ get_doc_link(child) }} + {% if this.is_child_of(child) %} +
      {{ loop(child.children) }}
    + {% endif %} + {% endfor %} +
+ + {% if this.body.toc %} +
+

This Page

+
    + {% for item in this.body.toc recursive %} +
  • {{ item.title }}{% + if item.children %}
      {{ loop(item.children) }}
    {% endif %} + {% endfor %} +
+
+ {% endif %} +
+ +
+ {{ get_doc_header(this) }} + + {% if this.type == 'method' %} +

Method of {{ get_doc_link(this.parent) }} + {% elif this.type == 'property' %} +

Property of {{ get_doc_link(this.parent) }} + {% elif this.type == 'operator' %} +

Operator of {{ get_doc_link(this.parent) }} + {% endif %} + {% if this.template_var %} +

Template variable: {{ this.template_var }} + {% endif %} + + {{ this.body }} + + {% if this.children %} +

+ {% for cols in this.children|batch(2) %} +
+ {% for page in cols if page %} +
+

+ {{ get_doc_icon(page) }} + {{ get_doc_link(page) }} +

+ {% if page.summary %} +

{{ page.summary }}

+ {% endif %} +
+ {% endfor %} +
+ {% endfor %} +
+ {% endif %} + + {% if this.allow_comments %} +
+

Comments

+ {{ render_disqus_comments() }} +
+ {% endif %} +
+
+{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 00000000..353d6ed2 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,78 @@ + + + + + + +{% block title %}Hello{% endblock %} | Lektor Static Website Engine + + + +
+ {% block outerbody %} +
+ {% block body %}{% endblock %} +
+ {% endblock %} +
+ + {% block bottomsummary %} +
+
+
+
+ {% endblock %} + + {% block footer %} + + {% endblock %} + + + + diff --git a/templates/macros/blocksupport.html b/templates/macros/blocksupport.html new file mode 100644 index 00000000..7a22ea07 --- /dev/null +++ b/templates/macros/blocksupport.html @@ -0,0 +1,37 @@ +{# + + This module implements some common logic to special case some blocks + so that they render over the entire width of the page. This is done + because there is no nice way via CSS to make child elements extend + past the parent's container. + + This also special cases the first banner. If the first item is a + banner it can inject the calling macro right after it. + +#} + +{% set full_width_blocks = ['banner', 'slideshow'] %} + +{% macro render_container(item, classes) %} +
+
+
+ {{ item }} +
+
+
+{% endmacro %} + +{% macro render_full_width_blocks(blocks, classes='col-md-8 col-md-offset-2') %} + {% if blocks and blocks[0]._flowblock != 'banner' and caller %} + {{ render_container(caller(), classes) }} + {% endif %} + {% for blk in blocks %} + {% if blk._flowblock in full_width_blocks %} + {{ blk }} + {% if loop.first and caller %}{{ render_container(caller(), classes) }}{% endif %} + {% else %} + {{ render_container(blk, classes) }} + {% endif %} + {% endfor %} +{% endmacro %} diff --git a/templates/macros/blog.html b/templates/macros/blog.html new file mode 100644 index 00000000..8bb175de --- /dev/null +++ b/templates/macros/blog.html @@ -0,0 +1,2 @@ +{% macro render_blog_post(post, from_index=false) %} +{% endmacro %} diff --git a/templates/macros/docs.html b/templates/macros/docs.html new file mode 100644 index 00000000..b2c76c55 --- /dev/null +++ b/templates/macros/docs.html @@ -0,0 +1,37 @@ +{% macro get_doc_icon(page) -%} + {% if page.type == 'method' or page.type == 'function' or page.type == 'filter' -%} + + {%- elif page.type == 'property' -%} + + {%- elif page.type == 'operator' -%} + + {%- elif page.type == 'class' -%} + + {%- elif page.type == 'cmdlet' -%} + + {%- elif page.type == 'event' -%} + + {%- elif page.type == 'type' -%} + + {%- else -%} + + {%- endif %} +{%- endmacro %} + +{% macro get_doc_link(page) -%} + {{ page.title }}{% + if page.type == 'function' or page.type == 'method' %}(){% endif %} +{%- endmacro %} + +{% macro get_doc_header(page) %} + {% if page.type %} +

{% if page.type %}{{ get_doc_icon(page) }} {% endif %}{% + if page.module %}{{ page.module }}.{% endif + %}{{ page.title }}{% + if page.type == 'method' or page.type == 'function' or page.type == 'filter' + or page.type == 'event' + %} ({{ page.signature }}){% endif %}

+ {% else %} +

{{ page.title }}

+ {% endif %} +{% endmacro %} diff --git a/templates/macros/pagination.html b/templates/macros/pagination.html new file mode 100644 index 00000000..b957977d --- /dev/null +++ b/templates/macros/pagination.html @@ -0,0 +1,15 @@ +{% macro render_pagination(pagination) %} + +{% endmacro %} diff --git a/templates/page.html b/templates/page.html new file mode 100644 index 00000000..e9afb20e --- /dev/null +++ b/templates/page.html @@ -0,0 +1,8 @@ +{% extends "layout.html" %} +{% from 'macros/blocksupport.html' import render_full_width_blocks %} +{% block title %}{{ this.title }}{% endblock %} +{% block outerbody %} + {% call render_full_width_blocks(this.body.blocks, classes='col-md-12') %} +

{{ this.title }}

+ {% endcall %} +{% endblock %} diff --git a/templates/poster.html b/templates/poster.html new file mode 100644 index 00000000..9c8968d2 --- /dev/null +++ b/templates/poster.html @@ -0,0 +1,7 @@ +{% extends "layout.html" %} +{% from 'macros/blocksupport.html' import render_full_width_blocks %} +{% block title %}{{ this.title }}{% endblock %} +{% block bodyclass %}poster{% endblock %} +{% block outerbody %} + {{ render_full_width_blocks(this.body.blocks, classes='col-md-12') }} +{% endblock %} diff --git a/templates/sitemap.xml b/templates/sitemap.xml new file mode 100644 index 00000000..772c3d8f --- /dev/null +++ b/templates/sitemap.xml @@ -0,0 +1,7 @@ + + + {%- for page in [site.root] if page != this recursive %} + {{ page|url(external=true) }} + {{- loop(page.children) }} + {%- endfor %} + diff --git a/webpack/images/arches.png b/webpack/images/arches.png new file mode 100644 index 00000000..6a50e1e2 Binary files /dev/null and b/webpack/images/arches.png differ diff --git a/webpack/images/curls.png b/webpack/images/curls.png new file mode 100644 index 00000000..2bda3eaa Binary files /dev/null and b/webpack/images/curls.png differ diff --git a/webpack/images/dark-curls.png b/webpack/images/dark-curls.png new file mode 100644 index 00000000..3361adfa Binary files /dev/null and b/webpack/images/dark-curls.png differ diff --git a/webpack/images/mesh.png b/webpack/images/mesh.png new file mode 100644 index 00000000..05825ad0 Binary files /dev/null and b/webpack/images/mesh.png differ diff --git a/webpack/images/paper.png b/webpack/images/paper.png new file mode 100644 index 00000000..6ac86c78 Binary files /dev/null and b/webpack/images/paper.png differ diff --git a/webpack/images/swirl.png b/webpack/images/swirl.png new file mode 100644 index 00000000..d9c6d818 Binary files /dev/null and b/webpack/images/swirl.png differ diff --git a/webpack/js/app.js b/webpack/js/app.js new file mode 100644 index 00000000..2ca3d0f2 --- /dev/null +++ b/webpack/js/app.js @@ -0,0 +1,94 @@ +require('bootstrap'); + +function selectText(text) { + if (document.body.createTextRange) { + let range = document.body.createTextRange(); + range.moveToElementText(text); + range.select(); + } else if (window.getSelection) { + let selection = window.getSelection(); + let range = document.createRange(); + range.selectNodeContents(text); + selection.removeAllRanges(); + selection.addRange(range); + } +} + +function initDownloadButton() { + let buttons = $('.download-btn'); + if (buttons.length <= 0) { + return; + } + + buttons.hide(); + $.ajax({ + method: 'GET', + url: 'https://api.github.com/repos/lektor/lektor/releases', + crossDomain: true + }).then((releases) => { + updateDownloadButtons(buttons.toArray(), releases); + }, () => { + buttons.show(); + }); +} + +function findBestTarget(assets) { + let matcher = null; + let note = null; + if (navigator.platform.match(/^mac/i)) { + matcher = /\.dmg$/; + note = 'For OSX 10.9 and later.'; + } + + if (matcher != null) { + for (let i = 0; i < assets.length; i++) { + if (assets[i].name.match(matcher)) { + return { + url: assets[i].browser_download_url, + note: note + }; + } + } + } + + return null; +} + +function updateDownloadButtons(buttons, releases) { + let tag = releases[0].tag_name; + let selectTarget = '/downloads/'; + let assetTarget = findBestTarget(releases[0].assets); + + buttons.forEach((button) => { + let note = $('
').appendTo(button); + let link = $('a', button); + + if (assetTarget) { + link.attr('href', assetTarget.url); + note.append($('').text(assetTarget.note + ' ')); + note.append( + $('Other platforms') + .attr('href', selectTarget)); + } else { + link.attr('href', selectTarget); + } + + link.append($('').text(tag)); + + $(button).fadeIn('slow'); + }); +} + +function initInstallRow() { + let code = $('.install-row pre'); + if (code.length > 0) { + code.on('dblclick', function() { + selectText(this); + }); + } +} + +$(function() { + initDownloadButton(); + initInstallRow(); +}); diff --git a/webpack/package.json b/webpack/package.json new file mode 100644 index 00000000..0ff36c23 --- /dev/null +++ b/webpack/package.json @@ -0,0 +1,26 @@ +{ + "name": "lektor-gui", + "version": "1.0.0", + "description": "Lektor graphical user interface", + "private": true, + "main": "static/gen/host.js", + "scripts": {}, + "devDependencies": { + "babel": "^5.8.23", + "babel-core": "^5.8.23", + "babel-eslint": "^4.1.1", + "babel-loader": "^5.3.2", + "bootstrap": "^3.3.5", + "bootstrap-sass": "^3.3.5", + "css-loader": "^0.9.1", + "extract-text-webpack-plugin": "^0.9.1", + "file-loader": "^0.8.1", + "jquery": "^2.1.4", + "node-sass": "^3.4.2", + "sass-loader": "^3.1.1", + "style-loader": "^0.8.3", + "url-loader": "^0.5.6", + "webpack": "^1.12.6", + "font-awesome": "^4.5.0" + } +} diff --git a/webpack/scss/main.scss b/webpack/scss/main.scss new file mode 100644 index 00000000..a9c78fc4 --- /dev/null +++ b/webpack/scss/main.scss @@ -0,0 +1,573 @@ +@charset "UTF-8"; + +@import url(https://fonts.googleapis.com/css?family=Roboto+Slab:400,300,300italic,400italic,500,500italic); + +$font-family-serif: 'Roboto Slab', 'Georgia', 'Times New Roman', serif; +$font-family-base: $font-family-serif; +$font-size-base: 16px; +$font-size-h1: floor(($font-size-base * 2.4)); +$font-size-h2: floor(($font-size-base * 2.1)); +$font-size-h3: ceil(($font-size-base * 1.5)); +$font-size-h4: ceil(($font-size-base * 1.3)); +$font-size-h5: $font-size-base; +$font-size-h6: ceil(($font-size-base * 0.85)); + +$container-large-desktop: 900px; + +$border-radius-base: 0; +$border-radius-large: 0; +$border-radius-small: 0; + +$brand-primary: #77304c; +$gray: #3d3b3c; +$toner: #111; +$lektor-header-bg: #3d3639; +$navbar-inverse-bg: $lektor-header-bg; + +$code-color: #77304c; +$code-bg: #f8eff3; + +$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"; +@import "~bootstrap-sass/assets/stylesheets/bootstrap"; + +$fa-font-path: "~font-awesome/fonts"; +@import "~font-awesome/scss/font-awesome.scss"; + +body { + padding: 0; + background: lighten($gray, 20) url(../images/mesh.png); + background-size: 256px 256px; +} + +blockquote { + border-left: none; + font-size: inherit; + padding: 0; + margin: 15px 0 15px 30px; +} + +nav.navbar-static-top { + margin-bottom: 0; +} + +div.body-wrapper { + background: white url(../images/curls.png); + background-position: 51px 0; + min-height: 500px; + padding-bottom: 60px; +} + +footer { + padding: 10px 0; + color: #333; + + a { + color: white; + } + + div.row > div + div { + text-align: right; + } +} + +pre { + border: none; + border-radius: 0; + background-image: url(../images/paper.png); + background-size: 300px 300px; + padding: 15px 20px; +} + +div.slideshow-wrapper { + overflow: hidden; +} + +div.slideshow { + margin: 20px -10px; + padding: 15px 10px; + height: auto; + overflow: auto; + background: lighten($gray, 55) url(../images/dark-curls.png); + box-shadow: inset 0 0 10px darken($lektor-header-bg, 5); + color: white; + + div.slideshow-inner { + left: 0; + right: 0; + height: auto; + overflow: hidden; + } + + .carousel-control { + background: transparent!important; + } + + div.slide { + div.container { + position: relative; + } + .description { + position: absolute; + left: 200px; + right: 200px; + background: fade-out($lektor-header-bg, 0.15); + border-radius: 5px; + bottom: 20px; + padding: 10px; + p { + margin: 0; + text-align: center; + } + + @media (max-width: $screen-sm-max) { + left: 20px; + right: 20px; + } + } + + img { + display: block; + margin: 15px auto; + box-shadow: 0 4px 16px fade-out($gray, 0.6); + border-radius: 6px; + height: 450px; + + @media (max-width: $screen-sm-max) { + height: auto; + width: 100%; + } + } + } +} + +body.poster { + h2 { + color: $brand-primary; + } +} + +body.default { + h2 { + font-weight: 100; + margin: 25px 0 15px 0; + } +} + +div.pagination { + display: block; + text-align: center; +} + +div.blog-snippet { + line-height: 1.6; + margin: 25px 0; + + h2, p { + line-height: 1.6; + display: inline; + font-size: $font-size-base; + font-weight: normal; + padding: 0; + margin: 0; + } + + h2 { + font-style: italic; + &:after { + content: " ― "; + } + } + + a.more { + white-space: nowrap; + } + + p.meta { + display: block; + text-align: right; + font-size: 0.9em; + } +} + +div.doc-styling, div.blog-post { + line-height: 1.75; + + a.ref { + font-style: italic; + } + + a.ext { + &:before { + @extend .glyphicon; + @extend .glyphicon-link; + color: $toner; + padding-right: 2px; + } + } + + div.child-pages { + border-top: 1px solid #ddd; + margin-top: 35px; + padding-top: 10px; + + div.child { + margin: 10px 0; + } + + h4, p { + margin: 0; + line-height: 1.4; + } + + h4 code.tech-name { + background: transparent; + font-size: 0.8em; + color: inherit; + font-style: italic; + } + } +} + +div.blog-post { + padding: 0 0 50px 0; + + div.meta-bar { + margin: 25px 0; + text-align: center; + h1 { + margin-bottom: 0; + } + } +} + +div.banner { + margin: 15px 0; + background-size: cover; + background-position: center center; + + &:first-child { + margin-top: 0; + box-shadow: 0 4px 10px fade-out($gray, 0.8); + } + + &.banner-dark { + background-color: $lektor-header-bg; + } +} + +div.doc-styling { + h1 { + code { + padding: 0!important; + display: inline; + background: transparent; + } + code.mod { + color: $toner; + } + span.sig { + font-size: 0.7em; + color: #888; + code { + font-family: $font-family-base; + color: $toner; + letter-spacing: -1px; + } + } + } + + p.type-info { + font-style: italic; + } + p.type-info + p.type-info { + margin-top: 0; + } + + table { + width: 100%; + border: 1px solid $brand-primary; + margin: 15px 0; + + th { + border-bottom: 1px solid $brand-primary; + background: $brand-primary; + color: white; + font-size: 13px; + } + + td, th { + padding: 5px 10px; + } + + tr:nth-child(odd) { + background: #f8f8f8; + } + } + + img { + max-width: 100%; + } + + img.screenshot { + width: 100%; + box-shadow: 0 4px 16px fade-out($gray, 0.8); + border-radius: 6px; + margin: 15px 0; + } +} + +div.screenshot-frame { + margin: 15px -30px 15px -30px; + padding: 30px; + background: white url(../images/swirl.png); + background-size: 256px 256px; + border-radius: 10px; + img { + width: 100%; + border-radius: 5px; + box-shadow: 0 0 6px fade-out($gray, 0.8); + } + @media (max-width: $screen-sm-max) { + margin: 15px 0; + padding: 0; + background: none; + } +} + +i.feature-circle { + width: 72px; + height: 72px; + text-align: center; + background: $brand-primary; + padding: 10px; + color: white; + border-radius: 50%; + margin: 0 0 15px 0; + line-height: 1; + font-size: 40px; + line-height: 52px; +} + +div.text-block { + margin: 15px 0; + &.text-block-centered { + text-align: center; + } + &.text-block-two-column-list { + text-align: center; + ul { + list-style: none; + margin: 0; + padding: 0; + font-size: 0; + li { + vertical-align: top; + font-size: $font-size-base; + margin: 15px 0; + padding: 0 50px; + width: 50%; + display: inline-block; + strong { + display: block; + font-size: 24px; + font-weight: normal; + } + @media (max-width: $screen-xs-max) { + width: 100%; + padding: 0 30px; + } + } + } + } +} + +div.pull-quote { + width: 25%; + margin-left: -200px; + background: green; + + &.pull-quote-left { + float: left; + } + &.pull-quote-right { + float: right; + } +} + +div.lektor-intro { + position: relative; + height: 100%; + //background: #3D2824; + + img { + margin-left: -80px; + max-height: 300px; + max-width: 700px; + + @media (max-width: $screen-md-max) { + width: 100%; + margin-left: 0; + } + } + + p { + text-shadow: 0 0 3px #3D3639; + color: white; + } + + a { + color: white; + font-weight: bold; + } + + div.install-row { + background: rgba(40, 30, 30, 0.8); + position: absolute; + bottom: 0; + left: 120px; + right: 120px; + + pre { + margin: 0; + background: transparent; + color: white; + } + + @media (max-width: $screen-md-max) { + left: 10px; + right: 10px; + } + } +} + +div.download-btn { + padding: 50px 0 0 0; + text-align: right; + + > a { + display: inline-block; + background: $brand-primary; + color: white; + border-radius: 7px; + padding: 10px 20px; + font-size: 17px; + text-decoration: none; + box-shadow: 3px 3px 0 fade-out($gray, 0.6); + margin-bottom: 3px; + + &:active { + margin-right: -3px; + margin-top: 3px; + margin-bottom: 0; + box-shadow: none; + } + + span.version:before { + content: " "; + } + } + + div.note { + margin-top: 10px; + color: white; + font-size: 0.8em; + } +} + +p.meta { + color: lighten($gray, 30); +} + +ul.toc, ul.toc ul { + font-size: 14px; + padding-left: 25px; +} + +ul.tree-nav { + margin: 25px 0; + padding: 0; + list-style: none; + + li { + padding: 0; + margin: 0; + + + li { + border-top: 1px solid #ddd; + } + + &.active { + > a { + font-weight: bold; + color: $toner; + } + } + + a { + display: block; + padding: 5px 10px; + + @media (max-width: $screen-sm-max) { + padding: 3px 10px; + font-size: 14px; + } + + @media (max-width: $screen-xs-max) { + padding: 1px 10px; + } + } + + @media (max-width: $screen-xs-max) { + border: none!important; + } + } + + ul { + list-style: none; + margin: 0; + padding: 0; + + li { + border-top: 1px solid #ddd; + padding-left: 10px; + margin-left: 0px; + font-size: 14px; + } + + li li { + padding-left: 20px; + margin-left: -10px; + } + + li li li { + padding-left: 30px; + margin-left: -20px; + } + + li li li li { + padding-left: 40px; + margin-left: -30px; + } + } +} + +div.comment-box { + border-top: 1px solid #ddd; + margin-top: 35px; +} + +div.sourceviewer { + dl { + dt { + width: 15%; + display: inline-block; + } + dd { + width: 75%; + display: inline-block; + } + } + pre { + overflow: auto; + } +} diff --git a/webpack/webpack.config.js b/webpack/webpack.config.js new file mode 100644 index 00000000..6d8758df --- /dev/null +++ b/webpack/webpack.config.js @@ -0,0 +1,55 @@ +var webpack = require('webpack'); +var path = require('path'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); + + +var options = { + entry: { + 'app': './js/app.js', + 'styles': './scss/main.scss' + }, + output: { + path: path.dirname(__dirname) + '/assets/static', + filename: '[name].js' + }, + devtool: '#cheap-module-source-map', + resolve: { + modulesDirectories: ['node_modules'], + extensions: ['', '.js'] + }, + module: { + loaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }, + { + test: /\.scss$/, + loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader') + }, + { + test: /\.css$/, + loader: ExtractTextPlugin.extract('style-loader', 'css-loader') + }, + { + test: /\.(woff2?|ttf|eot|svg|png)(\?.*?)?$/, + loader: 'file' + } + ] + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin(), + new webpack.optimize.DedupePlugin(), + new webpack.optimize.OccurenceOrderPlugin(), + new webpack.ProvidePlugin({ + $: 'jquery', + jQuery: 'jquery' + }), + new ExtractTextPlugin('styles.css', { + allChunks: true + }) + ] +}; + +module.exports = options;