jet

package module
Version: v6.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 5, 2021 License: Apache-2.0 Imports: 22 Imported by: 49

README

Jet Template Engine for Go

Build Status Build status

Jet is a template engine developed to be easy to use, powerful, dynamic, yet secure and very fast.

  • simple and familiar syntax
  • supports template inheritance (extends) and composition (block/yield, import, include)
  • descriptive error messages with filename and line number
  • auto-escaping
  • simple C-like expressions
  • very fast execution – Jet can execute templates faster than some pre-compiled template engines
  • very light in terms of allocations and memory footprint

v6

Version 6 brings major improvements to the Go API. Make sure to read through the breaking changes before making the jump.

Docs

Example application

An example to-do application is available in examples/todos. Clone the repository, then (in the repository root) do:

  $ cd examples/todos; go run main.go

IntelliJ Plugin

If you use IntelliJ there is a plugin available at https://github.com/jhsx/GoJetPlugin. There is also a very good Go plugin for IntelliJ – see https://github.com/go-lang-plugin-org/go-lang-idea-plugin. GoJetPlugin + Go-lang-idea-plugin = happiness!

Contributing

All contributions are welcome – if you find a bug please report it.

Contributors

  • José Santos (@jhsx)
  • Daniel Lohse (@annismckenzie)
  • Alexander Willing (@sauerbraten)

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsEmptyTree

func IsEmptyTree(n Node) bool

IsEmptyTree reports whether this tree (node) is empty of everything but space.

Types

type ActionNode

type ActionNode struct {
	NodeBase
	Set  *SetNode
	Pipe *PipeNode
}

ActionNode holds an action (something bounded by delimiters). Control actions have their own nodes; ActionNode represents simple ones such as field evaluations and parenthesized pipelines.

func (*ActionNode) String

func (a *ActionNode) String() string

type AdditiveExprNode

type AdditiveExprNode struct {
	// contains filtered or unexported fields
}

AdditiveExprNode represents an add or subtract expression ex: expression ( '+' | '-' ) expression

func (*AdditiveExprNode) String

func (node *AdditiveExprNode) String() string

type Arguments

type Arguments struct {
	// contains filtered or unexported fields
}

Arguments holds the arguments passed to jet.Func.

func (*Arguments) Get

func (a *Arguments) Get(argumentIndex int) reflect.Value

Get gets an argument by index.

func (*Arguments) IsSet

func (a *Arguments) IsSet(argumentIndex int) bool

IsSet checks whether an argument is set or not. It behaves like the build-in isset function.

func (*Arguments) NumOfArguments

func (a *Arguments) NumOfArguments() int

NumOfArguments returns the number of arguments

func (*Arguments) Panicf

func (a *Arguments) Panicf(format string, v ...interface{})

Panicf panics with formatted error message.

func (*Arguments) ParseInto

func (a *Arguments) ParseInto(ptrs ...interface{}) error

ParseInto parses the arguments into the provided pointers. It returns an error if the number of pointers passed in does not equal the number of arguments, if any argument's value is invalid according to Go's reflect package, if an argument can't be used as the value the pointer passed in at the corresponding position points to, or if an unhandled pointer type is encountered. Allowed pointer types are pointers to interface{}, int, int64, float64, bool, string, time.Time, reflect.Value, []interface{}, map[string]interface{}. If a pointer to a reflect.Value is passed in, the argument be assigned as-is to the value pointed to. For pointers to int or float types, type conversion is performed automatically if necessary.

func (*Arguments) RequireNumOfArguments

func (a *Arguments) RequireNumOfArguments(funcname string, min, max int)

RequireNumOfArguments panics if the number of arguments is not in the range specified by min and max. In case there is no minimum pass -1, in case there is no maximum pass -1 respectively.

func (*Arguments) Runtime

func (a *Arguments) Runtime() *Runtime

Runtime get the Runtime context

type BlockNode

type BlockNode struct {
	NodeBase        //The line number in the input. Deprecated: Kept for compatibility.
	Name     string //The name of the template (unquoted).

	Parameters *BlockParameterList
	Expression Expression //The command to evaluate as dot for the template.

	List    *ListNode
	Content *ListNode
}

BlockNode represents a {{block }} action.

func (*BlockNode) String

func (t *BlockNode) String() string

type BlockParameter

type BlockParameter struct {
	Identifier string
	Expression Expression
}

type BlockParameterList

type BlockParameterList struct {
	NodeBase
	List []BlockParameter
}

func (*BlockParameterList) Param

func (bplist *BlockParameterList) Param(name string) (Expression, int)

func (*BlockParameterList) String

func (bplist *BlockParameterList) String() (str string)

type BoolNode

type BoolNode struct {
	NodeBase
	True bool //The value of the boolean constant.
}

BoolNode holds a boolean constant.

func (*BoolNode) String

func (b *BoolNode) String() string

type BranchNode

type BranchNode struct {
	NodeBase
	Set        *SetNode
	Expression Expression
	List       *ListNode
	ElseList   *ListNode
}

BranchNode is the common representation of if, range, and with.

func (*BranchNode) String

func (b *BranchNode) String() string

type Cache

type Cache interface {

	// Get fetches a template from the cache. If Get returns nil, the same path with a different extension will be tried.
	// If Get() returns nil for all configured extensions, the same path and extensions will be tried on the Set's Loader.
	Get(templatePath string) *Template

	// Put places the result of parsing a template "file"/string in the cache.
	Put(templatePath string, t *Template)
}

Cache is the interface Jet uses to store and retrieve parsed templates.

type CallArgs

type CallArgs struct {
	Exprs       []Expression
	HasPipeSlot bool
}

type CallExprNode

type CallExprNode struct {
	NodeBase
	BaseExpr Expression
	CallArgs
}

CallExprNode represents a call expression ex: expression '(' (expression (',' expression)* )? ')'

func (*CallExprNode) String

func (s *CallExprNode) String() string

type ChainNode

type ChainNode struct {
	NodeBase
	Node  Node
	Field []string //The identifiers in lexical order.
}

ChainNode holds a term followed by a chain of field accesses (identifier starting with '.'). The names may be chained ('.x.y'). The periods are dropped from each ident.

func (*ChainNode) Add

func (c *ChainNode) Add(field string)

Add adds the named field (which should start with a period) to the end of the chain.

func (*ChainNode) String

func (c *ChainNode) String() string

type CommandNode

type CommandNode struct {
	NodeBase
	CallExprNode
}

CommandNode holds a command (a pipeline inside an evaluating action).

func (*CommandNode) String

func (c *CommandNode) String() string

type ComparativeExprNode

type ComparativeExprNode struct {
	// contains filtered or unexported fields
}

ComparativeExprNode represents a comparative expression ex: expression ( '==' | '!=' ) expression

func (*ComparativeExprNode) String

func (node *ComparativeExprNode) String() string

type Expression

type Expression interface {
	Node
}

type FieldNode

type FieldNode struct {
	NodeBase
	Ident []string //The identifiers in lexical order.
}

FieldNode holds a field (identifier starting with '.'). The names may be chained ('.x.y'). The period is dropped from each ident.

func (*FieldNode) String

func (f *FieldNode) String() string

type Func

type Func func(Arguments) reflect.Value

Func function implementing this type is called directly, which is faster than calling through reflect. If a function is being called many times in the execution of a template, you may consider implementing a wrapper for that function implementing a Func.

type IdentifierNode

type IdentifierNode struct {
	NodeBase
	Ident string //The identifier's name.
}

IdentifierNode holds an identifier.

func (*IdentifierNode) String

func (i *IdentifierNode) String() string

type IfNode

type IfNode struct {
	BranchNode
}

IfNode represents an {{if}} action and its commands.

type InMemLoader

type InMemLoader struct {
	// contains filtered or unexported fields
}

InMemLoader is a simple in-memory loader storing template contents in a simple map. InMemLoader normalizes paths passed to its methods by converting any input path to a slash-delimited path, turning it into an absolute path by prepending a "/" if neccessary, and cleaning it (see path.Clean()). It is safe for concurrent use.

func NewInMemLoader

func NewInMemLoader() *InMemLoader

NewInMemLoader return a new InMemLoader.

func (*InMemLoader) Delete

func (l *InMemLoader) Delete(templatePath string)

Delete removes whatever contents are stored under the given path.

func (*InMemLoader) Exists

func (l *InMemLoader) Exists(templatePath string) bool

Exists returns whether or not a template is indexed under this path.

func (*InMemLoader) Open

func (l *InMemLoader) Open(templatePath string) (io.ReadCloser, error)

Open returns a template's contents, or an error if no template was added under this path using Set().

func (*InMemLoader) Set

func (l *InMemLoader) Set(templatePath, contents string)

Set adds a template to the loader.

type IncludeNode

type IncludeNode struct {
	NodeBase
	Name    Expression
	Context Expression
}

IncludeNode represents a {{include }} action.

func (*IncludeNode) String

func (t *IncludeNode) String() string

type IndexExprNode

type IndexExprNode struct {
	NodeBase
	Base  Expression
	Index Expression
}

func (*IndexExprNode) String

func (s *IndexExprNode) String() string

type ListNode

type ListNode struct {
	NodeBase
	Nodes []Node //The element nodes in lexical order.
}

ListNode holds a sequence of nodes.

func (*ListNode) String

func (l *ListNode) String() string

type Loader

type Loader interface {
	// Exists returns whether or not a template exists under the requested path.
	Exists(templatePath string) bool

	// Open returns the template's contents or an error if something went wrong.
	// Calls to Open() will always be preceded by a call to Exists() with the same `templatePath`.
	// It is the caller's duty to close the template.
	Open(templatePath string) (io.ReadCloser, error)
}

Loader is a minimal interface required for loading templates.

Jet will build an absolute path (with slash delimiters) before looking up templates by resolving paths in extends/import/include statements:

- `{{ extends "/bar.jet" }}` will make Jet look up `/bar.jet` in the Loader unchanged, no matter where it occurs (since it's an absolute path) - `{{ include("\views\bar.jet") }}` will make Jet look up `/views/bar.jet` in the Loader, no matter where it occurs - `{{ import "bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet` - `{{ extends "./bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet` - `{{ import "../views\bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet` - `{{ include("../bar.jet") }}` in `/views/foo.jet` will result in a lookup of `/bar.jet` - `{{ import "../views/../bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/bar.jet`

This means that the same template will always be looked up using the same path.

Jet will also try appending multiple file endings for convenience: `{{ extends "/bar" }}` will lookup `/bar`, `/bar.jet`, `/bar.html.jet` and `/bar.jet.html` (in that order). To avoid unneccessary lookups, use the full file name in your templates (so the first lookup is always a hit, or override this list of extensions using Set.SetExtensions().

type LogicalExprNode

type LogicalExprNode struct {
	// contains filtered or unexported fields
}

LogicalExprNode represents a boolean expression, 'and' or 'or' ex: expression ( '&&' | '||' ) expression

func (*LogicalExprNode) String

func (node *LogicalExprNode) String() string

type MultiplicativeExprNode

type MultiplicativeExprNode struct {
	// contains filtered or unexported fields
}

MultiplicativeExprNode represents a multiplication, division, or module expression ex: expression ( '*' | '/' | '%' ) expression

func (*MultiplicativeExprNode) String

func (node *MultiplicativeExprNode) String() string

type NilNode

type NilNode struct {
	NodeBase
}

NilNode holds the special identifier 'nil' representing an untyped nil constant.

func (*NilNode) String

func (n *NilNode) String() string

type Node

type Node interface {
	Type() NodeType
	String() string
	Position() Pos
	// contains filtered or unexported methods
}

type NodeBase

type NodeBase struct {
	TemplatePath string
	Line         int
	NodeType
	Pos
}

type NodeType

type NodeType int

NodeType identifies the type of a parse tree node.

const (
	NodeText       NodeType = iota //Plain text.
	NodeAction                     //A non-control action such as a field evaluation.
	NodeChain                      //A sequence of field accesses.
	NodeCommand                    //An element of a pipeline.
	NodeField                      //A field or method name.
	NodeIdentifier                 //An identifier; always a function name.
	NodeUnderscore                 //An underscore (discard in assignment, or slot in argument list for piped value)
	NodeList                       //A list of Nodes.
	NodePipe                       //A pipeline of commands.
	NodeSet
	//NodeWith                       //A with action.
	NodeInclude
	NodeBlock

	NodeYield

	NodeIf //An if action.

	NodeRange //A range action.
	NodeTry

	NodeReturn

	NodeString //A string constant.
	NodeNil    //An untyped nil constant.
	NodeNumber //A numerical constant.
	NodeBool   //A boolean constant.
	NodeAdditiveExpr
	NodeMultiplicativeExpr
	NodeComparativeExpr
	NodeNumericComparativeExpr
	NodeLogicalExpr
	NodeCallExpr
	NodeNotExpr
	NodeTernaryExpr
	NodeIndexExpr
	NodeSliceExpr
)

func (NodeType) Type

func (t NodeType) Type() NodeType

Type returns itself and provides an easy default implementation for embedding in a Node. Embedded in all non-trivial Nodes.

type NotExprNode

type NotExprNode struct {
	NodeBase
	Expr Expression
}

NotExprNode represents a negate expression ex: '!' expression

func (*NotExprNode) String

func (s *NotExprNode) String() string

type NumberNode

type NumberNode struct {
	NodeBase

	IsInt      bool       //Number has an integral value.
	IsUint     bool       //Number has an unsigned integral value.
	IsFloat    bool       //Number has a floating-point value.
	IsComplex  bool       //Number is complex.
	Int64      int64      //The signed integer value.
	Uint64     uint64     //The unsigned integer value.
	Float64    float64    //The floating-point value.
	Complex128 complex128 //The complex value.
	Text       string     //The original textual representation from the input.
}

NumberNode holds a number: signed or unsigned integer, float, or complex. The value is parsed and stored under all the types that can represent the value. This simulates in a small amount of code the behavior of Go's ideal constants.

func (*NumberNode) String

func (n *NumberNode) String() string

type NumericComparativeExprNode

type NumericComparativeExprNode struct {
	// contains filtered or unexported fields
}

NumericComparativeExprNode represents a numeric comparative expression ex: expression ( '<' | '>' | '<=' | '>=' ) expression

func (*NumericComparativeExprNode) String

func (node *NumericComparativeExprNode) String() string

type OSFileSystemLoader

type OSFileSystemLoader struct {
	// contains filtered or unexported fields
}

OSFileSystemLoader implements Loader interface using OS file system (os.File).

func NewOSFileSystemLoader

func NewOSFileSystemLoader(dirPath string) *OSFileSystemLoader

NewOSFileSystemLoader returns an initialized OSFileSystemLoader.

func (*OSFileSystemLoader) Exists

func (l *OSFileSystemLoader) Exists(templatePath string) bool

Exists returns true if a file is found under the template path after converting it to a file path using the OS's path seperator and joining it with the loader's directory path.

func (*OSFileSystemLoader) Open

func (l *OSFileSystemLoader) Open(templatePath string) (io.ReadCloser, error)

Open returns the result of `os.Open()` on the file located using the same logic as Exists().

type Option

type Option func(*Set)

Option is the type of option functions that can be used in NewSet().

func InDevelopmentMode

func InDevelopmentMode() Option

InDevelopmentMode returns an option function that toggles development mode on, meaning the cache will always be bypassed and every template lookup will go to the loader.

func WithCache

func WithCache(c Cache) Option

WithCache returns an option function that sets the cache to use for template parsing results. Use InDevelopmentMode() to disable caching of parsed templates. By default, Jet uses a concurrency-safe in-memory cache that holds templates forever.

func WithDelims

func WithDelims(left, right string) Option

WithDelims returns an option function that sets the delimiters to the specified strings. Parsed templates will inherit the settings. Not setting them leaves them at the default: `{{` and `}}`.

func WithSafeWriter

func WithSafeWriter(w SafeWriter) Option

WithSafeWriter returns an option function that sets the escaping function to use when executing templates. By default, Jet uses a writer that takes care of HTML escaping. Pass nil to disable escaping.

func WithTemplateNameExtensions

func WithTemplateNameExtensions(extensions []string) Option

WithTemplateNameExtensions returns an option function that sets the extensions to try when looking up template names in the cache or loader. Default extensions are `""` (no extension), `".jet"`, `".html.jet"`, `".jet.html"`. Extensions will be tried in the order they are defined in the slice. WithTemplateNameExtensions panics when you pass in a nil or empty slice.

type PipeNode

type PipeNode struct {
	NodeBase                //The line number in the input. Deprecated: Kept for compatibility.
	Cmds     []*CommandNode //The commands in lexical order.
}

PipeNode holds a pipeline with optional declaration

func (*PipeNode) String

func (p *PipeNode) String() string

type Pos

type Pos int

Pos represents a byte position in the original input text from which this template was parsed.

func (Pos) Position

func (p Pos) Position() Pos

type RangeNode

type RangeNode struct {
	BranchNode
}

RangeNode represents a {{range}} action and its commands.

type Ranger

type Ranger interface {
	Range() (reflect.Value, reflect.Value, bool)
	ProvidesIndex() bool
}

Ranger describes an interface for types that iterate over something. Implementing this interface means the ranger will be used when it's encountered on the right hand side of a range's "let" expression.

type Renderer

type Renderer interface {
	Render(*Runtime)
}

Renderer is used to detect if a value has its own rendering logic. If the value an action evaluates to implements this interface, it will not be printed using github.com/CloudyKit/fastprinter, instead, its Render() method will be called and is responsible for writing the value to the render output.

type RendererFunc

type RendererFunc func(*Runtime)

RendererFunc func implementing interface Renderer

func (RendererFunc) Render

func (renderer RendererFunc) Render(r *Runtime)

type ReturnNode

type ReturnNode struct {
	NodeBase
	Value Expression
}

func (*ReturnNode) String

func (n *ReturnNode) String() string

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

Runtime this type holds the state of the execution of an template

func (*Runtime) Context

func (r *Runtime) Context() reflect.Value

Context returns the current context value

func (*Runtime) Let

func (state *Runtime) Let(name string, val interface{})

Let initialises a variable in the current template scope (possibly shadowing an existing variable of the same name in a parent scope).

func (*Runtime) LetGlobal

func (state *Runtime) LetGlobal(name string, val interface{})

LetGlobal sets or initialises a variable in the top-most template scope.

func (*Runtime) MustResolve

func (state *Runtime) MustResolve(name string) reflect.Value

Resolve calls resolve() and panics if there is an error.

func (*Runtime) Resolve

func (state *Runtime) Resolve(name string) reflect.Value

Resolve calls resolve() and ignores any errors, meaning it may return a zero reflect.Value.

func (*Runtime) Set

func (state *Runtime) Set(name string, val interface{}) error

Set sets an existing variable in the template scope it lives in.

func (*Runtime) SetOrLet

func (state *Runtime) SetOrLet(name string, val interface{})

SetOrLet calls Set() (if a variable with the given name is visible from the current scope) or Let() (if there is no variable with the given name in the current or any parent scope).

func (Runtime) Write

func (w Runtime) Write(b []byte) (int, error)

func (*Runtime) YieldBlock

func (st *Runtime) YieldBlock(name string, context interface{})

YieldBlock yields a block in the current context, will panic if the context is not available

type SafeWriter

type SafeWriter func(io.Writer, []byte)

SafeWriter is a function that writes bytes directly to the render output, without going through Jet's auto-escaping phase. Use/implement this if content should be escaped differently or not at all (see raw/unsafe builtins).

type Set

type Set struct {
	// contains filtered or unexported fields
}

Set is responsible to load, parse and cache templates. Every Jet template is associated with a Set.

func NewSet

func NewSet(loader Loader, opts ...Option) *Set

NewSet returns a new Set relying on loader. NewSet panics if a nil Loader is passed.

func (*Set) AddGlobal

func (s *Set) AddGlobal(key string, i interface{}) *Set

AddGlobal adds a global variable into the Set, overriding any value previously set under the specified key. It returns the Set it was called on to allow for method chaining.

func (*Set) AddGlobalFunc

func (s *Set) AddGlobalFunc(key string, fn Func) *Set

AddGlobalFunc adds a global function into the Set, overriding any function previously set under the specified key. It returns the Set it was called on to allow for method chaining.

func (*Set) GetTemplate

func (s *Set) GetTemplate(templatePath string) (t *Template, err error)

GetTemplate tries to find (and parse, if not yet parsed) the template at the specified path.

For example, GetTemplate("catalog/products.list") with extensions set to []string{"", ".html.jet",".jet"} will try to look for:

1. catalog/products.list
2. catalog/products.list.html.jet
3. catalog/products.list.jet

in the set's templates cache, and if it can't find the template it will try to load the same paths via the loader, and, if parsed successfully, cache the template (unless running in development mode).

func (*Set) LookupGlobal

func (s *Set) LookupGlobal(key string) (val interface{}, found bool)

LookupGlobal returns the global variable previously set under the specified key. It returns the nil interface and false if no variable exists under that key.

func (*Set) Parse

func (s *Set) Parse(templatePath, contents string) (template *Template, err error)

Parse parses `contents` as if it were located at `templatePath`, but won't put the result into the cache. Any referenced template (e.g. via `extends` or `import` statements) will be tried to be loaded from the cache. If a referenced template has to be loaded and parsed, it will also not be put into the cache after parsing.

type SetNode

type SetNode struct {
	NodeBase
	Let                bool
	IndexExprGetLookup bool
	Left               []Expression
	Right              []Expression
}

SetNode represents a set action, ident( ',' ident)* '=' expression ( ',' expression )*

func (*SetNode) String

func (set *SetNode) String() string

type SliceExprNode

type SliceExprNode struct {
	NodeBase
	Base     Expression
	Index    Expression
	EndIndex Expression
}

func (*SliceExprNode) String

func (s *SliceExprNode) String() string

type StringNode

type StringNode struct {
	NodeBase

	Quoted string //The original text of the string, with quotes.
	Text   string //The string, after quote processing.
}

StringNode holds a string constant. The value has been "unquoted".

func (*StringNode) String

func (s *StringNode) String() string

type Template

type Template struct {
	Name      string // name of the template represented by the tree.
	ParseName string // name of the top-level template during parsing, for error messages.

	Root *ListNode // top-level root of the tree.
	// contains filtered or unexported fields
}

Template is the representation of a single parsed template.

func (*Template) Execute

func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) (err error)

Execute executes the template into w.

func (*Template) String

func (t *Template) String() (template string)

type TernaryExprNode

type TernaryExprNode struct {
	NodeBase
	Boolean, Left, Right Expression
}

TernaryExprNod represents a ternary expression, ex: expression '?' expression ':' expression

func (*TernaryExprNode) String

func (s *TernaryExprNode) String() string

type TextNode

type TextNode struct {
	NodeBase
	Text []byte
}

TextNode holds plain text.

func (*TextNode) String

func (t *TextNode) String() string

type TryNode

type TryNode struct {
	NodeBase
	List  *ListNode
	Catch *catchNode
}

func (*TryNode) String

func (n *TryNode) String() string

type UnderscoreNode

type UnderscoreNode struct {
	NodeBase
}

UnderscoreNode is used for one of two things: - signals to discard the corresponding right side of an assignment - tells Jet where in a pipelined function call to inject the piped value

func (*UnderscoreNode) String

func (i *UnderscoreNode) String() string

type VarMap

type VarMap map[string]reflect.Value

func (VarMap) Set

func (scope VarMap) Set(name string, v interface{}) VarMap

func (VarMap) SetFunc

func (scope VarMap) SetFunc(name string, v Func) VarMap

func (VarMap) SetWriter

func (scope VarMap) SetWriter(name string, v SafeWriter) VarMap

func (VarMap) SortedKeys added in v6.1.0

func (scope VarMap) SortedKeys() []string

SortedKeys returns a sorted slice of VarMap keys

type YieldNode

type YieldNode struct {
	NodeBase          //The line number in the input. Deprecated: Kept for compatibility.
	Name       string //The name of the template (unquoted).
	Parameters *BlockParameterList
	Expression Expression //The command to evaluate as dot for the template.
	Content    *ListNode
	IsContent  bool
}

YieldNode represents a {{yield}} action

func (*YieldNode) String

func (t *YieldNode) String() string

Directories

Path Synopsis
examples
asset_packaging
+Build ignore
+Build ignore
todos
+Build ignore
+Build ignore
loaders

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL