contrib

package module
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Feb 20, 2025 License: Apache-2.0 Imports: 42 Imported by: 0

README

contrib

golang GitHub release pkg.go.dev Apache 2.0 license

Go 开发实用库

获取

go get -u github.com/yiigo/contrib

包含

  • xhash - 封装便于使用
  • xcrypto - 封装便于使用(支持 AES & RSA)
  • validator - 支持汉化和自定义规则
  • 基于 Redis 的分布式锁
  • 基于 sqlx 的轻量SQLBuilder
  • 基于泛型的无限菜单分类层级树
  • linklist - 一个并发安全的双向列表
  • errgroup - 基于官方版本改良,支持并发协程数量控制
  • xvalue - 用于处理 k-v 格式化的场景,如:生成签名串 等
  • xcoord - 距离、方位角、经纬度与平面直角坐标系的相互转化
  • timewheel - 简单实用的单层时间轮(支持一次性和多次重试任务)
  • 实用的辅助方法:IP、file、time、slice、string、version compare 等

⚠️ 注意:如需支持协程并发复用的 errgrouptimewheel,请使用 👉 nightfall

SQL Builder

⚠️ 目前支持的特性有限,复杂的SQL(如:子查询等)还需自己手写

builder := contrib.NewSQLBuilder(*sqlx.DB, func(ctx context.Context, query string, args ...any) {
    fmt.Println(query, args)
})
👉 Query
ctx := context.Background()

type User struct {
    ID     int    `db:"id"`
    Name   string `db:"name"`
    Age    int    `db:"age"`
    Phone  string `db:"phone,omitempty"`
}

var (
    record User
    records []User
)

builder.Wrap(
    contrib.Table("user"),
    contrib.Where("id = ?", 1),
).One(ctx, &record)
// SELECT * FROM user WHERE (id = ?)
// [1]

builder.Wrap(
    contrib.Table("user"),
    contrib.Where("name = ? AND age > ?", "yiigo", 20),
).All(ctx, &records)
// SELECT * FROM user WHERE (name = ? AND age > ?)
// [yiigo 20]

builder.Wrap(
    contrib.Table("user"),
    contrib.Where("name = ?", "yiigo"),
    contrib.Where("age > ?", 20),
).All(ctx, &records)
// SELECT * FROM user WHERE (name = ?) AND (age > ?)
// [yiigo 20]

builder.Wrap(
    contrib.Table("user"),
    contrib.WhereIn("age IN (?)", []int{20, 30}),
).All(ctx, &records)
// SELECT * FROM user WHERE (age IN (?, ?))
// [20 30]

builder.Wrap(
    contrib.Table("user"),
    contrib.Select("id", "name", "age"),
    contrib.Where("id = ?", 1),
).One(ctx, &record)
// SELECT id, name, age FROM user WHERE (id = ?)
// [1]

builder.Wrap(
    contrib.Table("user"),
    contrib.Distinct("name"),
    contrib.Where("id = ?", 1),
).One(ctx, &record)
// SELECT DISTINCT name FROM user WHERE (id = ?)
// [1]

builder.Wrap(
    contrib.Table("user"),
    contrib.LeftJoin("address", "user.id = address.user_id"),
    contrib.Where("user.id = ?", 1),
).One(ctx, &record)
// SELECT * FROM user LEFT JOIN address ON user.id = address.user_id WHERE (user.id = ?)
// [1]

builder.Wrap(
    contrib.Table("address"),
    contrib.Select("user_id", "COUNT(*) AS total"),
    contrib.GroupBy("user_id"),
    contrib.Having("user_id = ?", 1),
).All(ctx, &records)
// SELECT user_id, COUNT(*) AS total FROM address GROUP BY user_id HAVING (user_id = ?)
// [1]

builder.Wrap(
    contrib.Table("user"),
    contrib.Where("age > ?", 20),
    contrib.OrderBy("age ASC", "id DESC"),
    contrib.Offset(5),
    contrib.Limit(10),
).All(ctx, &records)
// SELECT * FROM user WHERE (age > ?) ORDER BY age ASC, id DESC LIMIT ? OFFSET ?
// [20, 10, 5]

wrap1 := builder.Wrap(
    contrib.Table("user_1"),
    contrib.Where("id = ?", 2),
)

builder.Wrap(
    contrib.Table("user_0"),
    contrib.Where("id = ?", 1),
    contrib.Union(wrap1),
).All(ctx, &records)
// (SELECT * FROM user_0 WHERE (id = ?)) UNION (SELECT * FROM user_1 WHERE (id = ?))
// [1, 2]

builder.Wrap(
    contrib.Table("user_0"),
    contrib.Where("id = ?", 1),
    contrib.UnionAll(wrap1),
).All(ctx, &records)
// (SELECT * FROM user_0 WHERE (id = ?)) UNION ALL (SELECT * FROM user_1 WHERE (id = ?))
// [1, 2]

builder.Wrap(
    contrib.Table("user_0"),
    contrib.WhereIn("age IN (?)", []int{10, 20}),
    contrib.Limit(5),
    contrib.Union(
        builder.Wrap(
            contrib.Table("user_1"),
            contrib.Where("age IN (?)", []int{30, 40}),
            contrib.Limit(5),
        ),
    ),
).All(ctx, &records)
// (SELECT * FROM user_0 WHERE (age IN (?, ?)) LIMIT ?) UNION (SELECT * FROM user_1 WHERE (age IN (?, ?)) LIMIT ?)
// [10, 20, 5, 30, 40, 5]
👉 Insert
ctx := context.Background()

type User struct {
    ID     int64  `db:"-"`
    Name   string `db:"name"`
    Age    int    `db:"age"`
    Phone  string `db:"phone,omitempty"`
}

builder.Wrap(Table("user")).Insert(ctx, &User{
    Name: "yiigo",
    Age:  29,
})
// INSERT INTO user (name, age) VALUES (?, ?)
// [yiigo 29]

builder.Wrap(contrib.Table("user")).Insert(ctx, map[string]any{
    "name": "yiigo",
    "age":  29,
})
// INSERT INTO user (name, age) VALUES (?, ?)
// [yiigo 29]
👉 Batch Insert
ctx := context.Background()

type User struct {
    ID     int64  `db:"-"`
    Name   string `db:"name"`
    Age    int    `db:"age"`
    Phone  string `db:"phone,omitempty"`
}

builder.Wrap(Table("user")).BatchInsert(ctx, []*User{
    {
        Name: "yiigo",
        Age:  20,
    },
    {
        Name: "yiigo",
        Age:  29,
    },
})
// INSERT INTO user (name, age) VALUES (?, ?), (?, ?)
// [yiigo 20 yiigo 29]

builder.Wrap(contrib.Table("user")).BatchInsert(ctx, []map[string]any{
    {
        "name": "yiigo",
        "age":  20,
    },
    {
        "name": "yiigo",
        "age":  29,
    },
})
// INSERT INTO user (name, age) VALUES (?, ?), (?, ?)
// [yiigo 20 yiigo 29]
👉 Update
ctx := context.Background()

type User struct {
    Name   string `db:"name"`
    Age    int    `db:"age"`
    Phone  string `db:"phone,omitempty"`
}

builder.Wrap(
    contrib.Table("user"),
    contrib.Where("id = ?", 1),
).Update(ctx, &User{
    Name: "yiigo",
    Age:  29,
})
// UPDATE user SET name = ?, age = ? WHERE (id = ?)
// [yiigo 29 1]

builder.Wrap(
    contrib.Table("user"),
    contrib.Where("id = ?", 1),
).Update(ctx, map[string]any{
    "name": "yiigo",
    "age":  29,
})
// UPDATE user SET name = ?, age = ? WHERE (id = ?)
// [yiigo 29 1]

builder.Wrap(
    contrib.Table("product"),
    contrib.Where("id = ?", 1),
).Update(ctx, map[string]any{
    "price": contrib.SQLExpr("price * ? + ?", 2, 100),
})
// UPDATE product SET price = price * ? + ? WHERE (id = ?)
// [2 100 1]
👉 Delete
ctx := context.Background()

builder.Wrap(
    contrib.Table("user"),
    contrib.Where("id = ?", 1),
).Delete(ctx)
// DELETE FROM user WHERE id = ?
// [1]

builder.Wrap(contrib.Table("user")).Truncate(ctx)
// TRUNCATE user
👉 Transaction
builder.Transaction(context.Background(), func(ctx context.Context, tx contrib.TXBuilder) error {
    _, err := tx.Wrap(
        contrib.Table("address"),
        contrib.Where("user_id = ?", 1),
    ).Update(ctx, map[string]any{"default": 0})
    if err != nil {
        return err
    }

    _, err = tx.Wrap(
        contrib.Table("address"),
        contrib.Where("id = ?", 1),
    ).Update(ctx, map[string]any{"default": 1})

    return err
})

Enjoy 😊

Documentation

Index

Constants

View Source
const (
	HeaderAccept        = "Accept"
	HeaderAuthorization = "Authorization"
	HeaderContentType   = "Content-Type"
)
View Source
const (
	ContentText          = "text/plain; charset=utf-8"
	ContentJSON          = "application/json"
	ContentXML           = "application/xml"
	ContentForm          = "application/x-www-form-urlencoded"
	ContentStream        = "application/octet-stream"
	ContentMultipartForm = "multipart/form-data"
)
View Source
const (
	// B - Byte size
	B Quantity = 1
	// KiB - KibiByte size
	KiB = 1024 * B
	// MiB - MebiByte size
	MiB = 1024 * KiB
	// GiB - GibiByte size
	GiB = 1024 * MiB
	// TiB - TebiByte size
	TiB = 1024 * GiB
)
View Source
const MaxFormMemory = 32 << 20
View Source
const MediaThumbnailWidth = 200

Variables

View Source
var (
	// ErrSQLDataType 不合法的插入或更新数据类型错误
	ErrSQLDataType = errors.New("invaild data type, expects: struct, *struct, map[string]any")

	// ErrSQLBatchDataType 不合法的批量插入数据类型错误
	ErrSQLBatchDataType = errors.New("invaild data type, expects: []struct, []*struct, []map[string]any")
)
View Source
var GMT8 = time.FixedZone("CST", 8*3600)

GMT8 东八区时区

View Source
var RestyClient = resty.NewWithClient(NewHttpClient())

RestyClient default client for http request

Functions

func AddSlashes

func AddSlashes(s string) string

AddSlashes 在字符串的每个引号前添加反斜杠

func BindForm added in v1.0.3

func BindForm(r *http.Request, obj any) error

BindForm 解析Form表单并校验

func BindJSON added in v1.0.3

func BindJSON(r *http.Request, obj any) error

BindJSON 解析JSON请求体并校验

func BindProto added in v1.0.4

func BindProto(r *http.Request, msg proto.Message) error

BindProto 解析Proto请求体并校验

func ContentType

func ContentType(h http.Header) string

func CreateFile

func CreateFile(filename string) (*os.File, error)

CreateFile 创建或清空指定的文件 文件已存在,则清空;文件或目录不存在,则以0775权限创建

func DebugLogger

func DebugLogger(options ...zap.Option) *zap.Logger

func ExcelColumnIndex

func ExcelColumnIndex(name string) int

ExcelColumnIndex 返回Excel列名对应的序号,如:A=0,B=1,AA=26,AB=27

func IP2Long

func IP2Long(ip string) uint32

IP2Long IP地址转整数

func ImageCrop

func ImageCrop(w io.Writer, filename string, rect *Rect, options ...imaging.EncodeOption) error

ImageCrop 图片裁切

func ImageCropFromReader

func ImageCropFromReader(w io.Writer, r io.Reader, format imaging.Format, rect *Rect, options ...imaging.EncodeOption) error

ImageCropFromReader 图片裁切

func ImageLabel

func ImageLabel(w io.Writer, filename string, rects []*Rect, options ...imaging.EncodeOption) error

ImageLabel 图片标注

func ImageLabelFromReader

func ImageLabelFromReader(w io.Writer, r io.Reader, format imaging.Format, rects []*Rect, options ...imaging.EncodeOption) error

ImageLabelFromReader 图片标注

func ImageThumbnail

func ImageThumbnail(w io.Writer, filename string, rect *Rect, options ...imaging.EncodeOption) error

ImageThumbnail 图片缩略图

func ImageThumbnailFromReader

func ImageThumbnailFromReader(w io.Writer, r io.Reader, format imaging.Format, rect *Rect, options ...imaging.EncodeOption) error

ImageThumbnailFromReader 图片缩略图

func IsUniqueDuplicateError

func IsUniqueDuplicateError(err error) bool

IsUniqueDuplicateError 判断是否「唯一索引冲突」错误

func Long2IP

func Long2IP(ip uint32) string

Long2IP 整数转IP地址

func MapForm

func MapForm(ptr any, form map[string][]string) error

func MapFormByTag

func MapFormByTag(ptr any, form map[string][]string, tag string) error

func MapQuery

func MapQuery(ptr any, m map[string][]string) error

func MappingByPtr

func MappingByPtr(ptr any, setter setter, tag string) error

func MarshalNoEscapeHTML

func MarshalNoEscapeHTML(v any) ([]byte, error)

MarshalNoEscapeHTML 不带HTML转义的JSON序列化

func MyTimeEncoder

func MyTimeEncoder(t time.Time, e zapcore.PrimitiveArrayEncoder)

MyTimeEncoder 自定义时间格式化

func NewDB

func NewDB(cfg *DBConfig) (*sql.DB, error)

NewDB sql.DB

func NewDBx

func NewDBx(cfg *DBConfig) (*sqlx.DB, error)

NewDBx sqlx.DB

func NewHttpClient

func NewHttpClient() *http.Client

NewHttpClient returns a http client

func NewLogger

func NewLogger(cfg *LogConfig) *zap.Logger

func Nonce

func Nonce(size uint8) string

Nonce 生成随机串(size应为偶数)

func OpenFile

func OpenFile(filename string) (*os.File, error)

OpenFile 打开指定的文件 文件已存在,则追加内容;文件或目录不存在,则以0775权限创建

func QuoteMeta

func QuoteMeta(s string) string

QuoteMeta 在字符串预定义的字符前添加反斜杠

func Retry

func Retry(ctx context.Context, fn func(ctx context.Context) error, attempts int, sleep time.Duration) (err error)

Retry 重试

func SliceChunk

func SliceChunk[T ~[]E, E any](list T, size int) []T

SliceChunk 集合分片

func SliceDiff

func SliceDiff[T ~[]E, E comparable](list1 T, list2 T) (ret1 T, ret2 T)

SliceDiff 返回两个集合之间的差异

func SliceIn

func SliceIn[T ~[]E, E comparable](list T, elem E) bool

SliceIn 返回指定元素是否在集合中

func SliceIntersect

func SliceIntersect[T ~[]E, E comparable](list1 T, list2 T) T

SliceIntersect 返回两个集合的交集

func SlicePinTop

func SlicePinTop[T ~[]E, E any](list T, index int)

SlicePinTop 置顶集合中的一个元素

func SlicePinTopF

func SlicePinTopF[T ~[]E, E any](list T, fn func(v E) bool)

SlicePinTopF 置顶集合中满足条件的一个元素

func SliceRand

func SliceRand[T ~[]E, E any](list T, n int) T

SliceRand 返回一个指定随机挑选个数的切片 若 n == -1 or n >= len(list),则返回打乱的切片

func SliceUnion

func SliceUnion[T ~[]E, E comparable](lists ...T) T

SliceUnion 返回两个集合的并集

func SliceUniq

func SliceUniq[T ~[]E, E comparable](list T) T

SliceUniq 集合去重

func SliceWithout

func SliceWithout[T ~[]E, E comparable](list T, exclude ...E) T

SliceWithout 返回不包括所有给定值的切片

func StrToTime

func StrToTime(layout, datetime string, loc *time.Location) time.Time

StrToTime 时间字符串解析为时间戳

func StripSlashes

func StripSlashes(s string) string

StripSlashes 删除字符串中的反斜杠

func TimeToStr

func TimeToStr(layout string, timestamp int64, loc *time.Location) string

TimeToStr 时间戳格式化为时间字符串 若 timestamp < 0,则使用 `time.Now()`

func Transaction

func Transaction(ctx context.Context, db *sqlx.DB, fn func(ctx context.Context, tx *sqlx.Tx) error) (err error)

Transaction 执行数据库事物

func VersionCompare

func VersionCompare(rangeVer, curVer string) (bool, error)

VersionCompare 语义化的版本比较,支持:>, >=, =, !=, <, <=, | (or), & (and). 参数 `rangeVer` 示例:1.0.0, =1.0.0, >2.0.0, >=1.0.0&<2.0.0, <2.0.0|>3.0.0, !=4.0.4

func WeekAround

func WeekAround(layout string, now time.Time) (monday, sunday string)

WeekAround 返回给定时间戳所在周的「周一」和「周日」时间字符串

Types

type DBConfig

type DBConfig struct {
	// Driver 驱动名称
	Driver string
	// DSN 数据源名称
	//
	//  [-- MySQL] username:password@tcp(localhost:3306)/dbname?timeout=10s&charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=True&loc=Local
	//  [Postgres] host=localhost port=5432 user=root password=secret dbname=test search_path=schema connect_timeout=10 sslmode=disable
	//  [- SQLite] file::memory:?cache=shared
	DSN string
	// MaxOpenConns 设置最大可打开的连接数
	MaxOpenConns int
	// MaxIdleConns 连接池最大闲置连接数
	MaxIdleConns int
	// ConnMaxLifetime 连接的最大生命时长
	ConnMaxLifetime time.Duration
	// ConnMaxIdleTime 连接最大闲置时间
	ConnMaxIdleTime time.Duration
}

DBConfig 数据库初始化配置

type ILevelNode

type ILevelNode interface {
	GetID() int64
	GetPid() int64
}

ILevelNode 层级树泛型约束

type ImageEXIF

type ImageEXIF struct {
	Size        int64
	Format      string
	Width       int
	Height      int
	Orientation string
	Longitude   decimal.Decimal
	Latitude    decimal.Decimal
}

ImageEXIF 定义图片EXIF

func ParseImageEXIF

func ParseImageEXIF(filename string) (*ImageEXIF, error)

ParseImageEXIF 解析图片EXIF

type LevelTree

type LevelTree[T ILevelNode] struct {
	Data     T
	Children []*LevelTree[T]
}

LevelTree 菜单或分类层级树

func BuildLevelTree

func BuildLevelTree[T ILevelNode](data map[int64][]T, pid int64) []*LevelTree[T]

BuildLevelTree 构建菜单或分类层级树(data=按pid归类后的数据, pid=树的起始ID)

type LogConfig

type LogConfig struct {
	// Filename 日志名称
	Filename string
	// Level 日志级别
	Level zapcore.Level
	// MaxSize 当前文件多大时轮替;默认:100MB
	MaxSize int
	// MaxAge 轮替的旧文件最大保留时长;默认:不限
	MaxAge int
	// MaxBackups 轮替的旧文件最大保留数量;默认:不限
	MaxBackups int
	// Compress 轮替的旧文件是否压缩;默认:不压缩
	Compress bool
	// Stderr 是否输出到控制台
	Stderr bool
	// Options Zap日志选项
	Options []zap.Option
}

LogConfig 日志初始化配置

type Mutex added in v1.0.2

type Mutex interface {
	// Lock 获取锁
	Lock(ctx context.Context) (bool, error)
	// TryLock 尝试获取锁
	TryLock(ctx context.Context, attempts int, delay time.Duration) (bool, error)
	// UnLock 释放锁
	UnLock(ctx context.Context) error
}

Mutex 分布式锁

func RedLock added in v1.0.2

func RedLock(cli *redis.Client, key string, ttl time.Duration) Mutex

RedLock 基于Redis实现的分布式锁实例

type Orientation

type Orientation int

Orientation 图片的旋转方向

const (
	TopLeft     Orientation = 1
	TopRight    Orientation = 2
	BottomRight Orientation = 3
	BottomLeft  Orientation = 4
	LeftTop     Orientation = 5
	RightTop    Orientation = 6
	RightBottom Orientation = 7
	LeftBottom  Orientation = 8
)

func (Orientation) String

func (o Orientation) String() string

type Quantity

type Quantity int64

Quantity 字节大小

func (Quantity) String

func (q Quantity) String() string

String 实现 Stringer 接口

type Rect

type Rect struct {
	X int
	Y int
	W int
	H int
}

Rect 定义一个矩形框

type SQLBuilder

type SQLBuilder interface {
	TXBuilder
	// Transaction 启用事务
	Transaction(ctx context.Context, f func(ctx context.Context, tx TXBuilder) error) error
}

SQLBuilder SQL构造器

func NewSQLBuilder

func NewSQLBuilder(db *sqlx.DB, logFn func(ctx context.Context, query string, args ...any)) SQLBuilder

NewSQLBuilder 生成SQL构造器

type SQLClause

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

SQLClause SQL语句

func SQLExpr

func SQLExpr(query string, binds ...any) *SQLClause

SQLExpr 生成一个语句表达式,例如:contrib.SQLExpr("price * ? + ?", 2, 100)

type SQLOption

type SQLOption func(w *sqlWrapper)

SQLOption SQL查询选项

func CrossJoin

func CrossJoin(table string) SQLOption

CrossJoin 指定 `CROSS JOIN` 语句

func Distinct

func Distinct(columns ...string) SQLOption

Distinct 指定 `DISTINCT` 子句

func FullJoin

func FullJoin(table, on string) SQLOption

FullJoin 指定 `FULL JOIN` 子句

func GroupBy

func GroupBy(columns ...string) SQLOption

GroupBy 指定 `GROUP BY` 子句

func Having

func Having(query string, binds ...any) SQLOption

Having 指定 `HAVING` 子句

func Join

func Join(table, on string) SQLOption

Join 指定 `INNER JOIN` 子句

func LeftJoin

func LeftJoin(table, on string) SQLOption

LeftJoin 指定 `LEFT JOIN` 子句

func Limit

func Limit(n int) SQLOption

Limit 指定 `LIMIT` 子句

func Offset

func Offset(n int) SQLOption

Offset 指定 `OFFSET` 子句

func OrderBy

func OrderBy(columns ...string) SQLOption

OrderBy 指定 `ORDER BY` 子句

func Returning

func Returning(columns ...string) SQLOption

Returning 指定 `RETURNING` 子句; 用于 PostgresSQL 和 SQLite(3.35.0) `INSERT` 语句

func RightJoin

func RightJoin(table, on string) SQLOption

RightJoin 指定 `RIGHT JOIN` 子句

func Select

func Select(columns ...string) SQLOption

Select 指定查询字段名

func Table

func Table(name string) SQLOption

Table 指定查询表名称

func Union

func Union(wrappers ...SQLWrapper) SQLOption

Union 指定 `UNION` 子句

func UnionAll

func UnionAll(wrappers ...SQLWrapper) SQLOption

UnionAll 指定 `UNION ALL` 子句

func Where

func Where(query string, binds ...any) SQLOption

Where 指定 `WHERE` 子句

func WhereIn

func WhereIn(query string, binds ...any) SQLOption

WhereIn 指定 `IN` 子句

type SQLWrapper

type SQLWrapper interface {
	// One 查询一条数据
	One(ctx context.Context, data any) error
	// All 查询多条数据
	All(ctx context.Context, data any) error
	// Insert 插入数据 (数据类型:`struct`, `*struct`, `map[string]any`)
	Insert(ctx context.Context, data any) (sql.Result, error)
	// BatchInsert 批量插入数据 (数据类型:`[]struct`, `[]*struct`, `[]map[string]any`)
	BatchInsert(ctx context.Context, data any) (sql.Result, error)
	// Update 更新数据 (数据类型:`struct`, `*struct`, `map[string]any`)
	Update(ctx context.Context, data any) (sql.Result, error)
	// Delete 删除数据
	Delete(ctx context.Context) (sql.Result, error)
	// Truncate 清空表
	Truncate(ctx context.Context) (sql.Result, error)
}

SQLWrapper SQL包装器

type Step

type Step struct {
	Head int
	Tail int
}

func Steps

func Steps(total, step int) (steps []Step)

Steps calculates the steps.

Example:

arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
for _, step := range contrib.Steps(len(arr), 6) {
	cur := arr[step.Head:step.Tail]
	// todo: do something
}

type TXBuilder

type TXBuilder interface {
	// Wrap 包装查询选项
	Wrap(opts ...SQLOption) SQLWrapper
	// contains filtered or unexported methods
}

TXBuilder 事务构造器

type X

type X map[string]any

X 类型别名

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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