bbs

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Jan 10, 2024 License: MIT Imports: 9 Imported by: 1

README

Package bbs

Package bbs is a Go module that interacts with legacy textfiles encoded with Bulletin Board Systems (BBS) color codes to reconstruct them into HTML documents.

BBSes were popular in the 1980s and 1990s and allowed computer users to chat, message, and share files over the landline telephone network. The commercialization and ease of access to the Internet eventually replaced BBSes, as did the worldwide-web. These centralized systems, termed boards, used a text-based interface, and their owners often applied colorization, text themes, and art to differentiate themselves.

While in the 1990s, ANSI control codes were in everyday use on the PC/MS-DOS, the standard comes from mainframe equipment. Home microcomputers often had difficulty interpreting it. So, BBS developers created their own, more straightforward methods to colorize and theme the text output to solve this.

*Please note that many microcomputer, PC and MS-DOS based boards used ANSI control codes for colorizations that this library does not support.

Quick usage

Go Package with docs and examples.

// open the text file
file, err := os.Open("pcboard.txt")
if err != nil {
    log.Print(err)
    return
}
defer file.Close()

// transform the MS-DOS text to Unicode
decoder := charmap.CodePage437.NewDecoder()
reader := transform.NewReader(file, decoder)

// create the HTML equivalent of BBS color codes
var buf bytes.Buffer
cc, err := bbs.HTML(&buf, reader)
if err != nil {
    log.Print(err)
    return
}

// fetch CSS
var css bytes.Buffer
if err := cc.CSS(&css); err != nil {
    log.Print(err)
    return
}

// print the partial html and css
fmt.Fprintln(os.Stdout, css.String(), "\n", buf.String())

Known codes

PCBoard

One of the most well-known applications for hosting a PC/MS-DOS BBS, PCBoard pioneered the file_id.diz file descriptor, and being endlessly expandable through software plugins known as PPEs. It developed the popular @X color code and @ control syntax.

Celerity

Another PC/MS-DOS application was very popular with the hacking, phreaking, and pirate communities in the early 1990s. It introduced a unique | pipe code syntax in late 1991 that revised the code syntax in version 2 of the software.

Renegade

A PC/MS-DOS application that was a derivative of the source code of Telegard BBS. Surprisingly, there was a new release of this software in 2021. Renegade had two methods to implement color, and this library uses the Pipe Bar Color Codes.

Telegard

A PC/MS-DOS application became famous due to a source code leak or release by one of its authors in an era when most developers were still highly secretive with their code. The source is in use in several other BBS applications.

WWIV

A mainstay in the PC/MS-DOS BBS scene of the 1980s and early 1990s, the software became well-known for releasing its source code to registered users. It allowed owners to expand the code to incorporate additional software, such as games or utilities, and port it to other platforms. The source is now Open Source and is still updated. Confusingly, WWIV has three methods of colorizing text: 10 | pipe colors, two-digit pipe colors, and its original Heart Codes.

Wildcat

WILDCAT! was a popular, propriety PC/MS-DOS application from the late 1980s that later migrated to Windows. It was one of the few BBS applications sold at retail in a physical box. It extensively used @ color codes throughout later revisions of the software.

Documentation

Overview

Package bbs is a Go module that interacts with legacy textfiles encoded with Bulletin Board Systems (BBS) color codes to reconstruct them into HTML documents.

BBSes were popular in the 1980s and 1990s and allowed computer users to chat, message, and share files over the landline telephone network. The commercialization and ease of access to the Internet eventually replaced BBSes, as did the worldwide-web. These centralized systems, termed boards, used a text-based interface, and their owners often applied colorization, text themes, and art to differentiate themselves.

While in the 1990s, ANSI control codes were in everyday use on the PC/MS-DOS, the standard comes from mainframe equipment. Home microcomputers often had difficulty interpreting it. So, BBS developers created their own, more straightforward methods to colorize and theme the text output to solve this.

*Please note that many microcomputer, PC and MS-DOS based boards used ANSI control codes for colorizations that this library does not support.

PCBoard

One of the most well-known applications for hosting a PC/MS-DOS BBS, PCBoard pioneered the file_id.diz file descriptor, as well as being endlessly expandable through software plugins known as PPEs. It developed the popular @X color code and @ control syntax.

Celerity

Another PC/MS-DOS application that was very popular with the hacking, phreaking, and pirate communities in the early 1990s. It introduced a unique | pipe code syntax in late 1991 that revised the code syntax in version 2 of the software.

Renegade

A PC/MS-DOS application that was a derivative of the source code of Telegard BBS. Surprisingly there was a new release of this software in 2021. Renegade had two methods to implement color, and this library uses the Pipe Bar Color Codes.

Telegard

A PC/MS-DOS application became famous due to a source code leak or release by one of its authors back in an era when most developers were still highly secretive with their code. The source is incorporated into several other projects.

WVIV

A mainstay in the PC/MS-DOS BBS scene of the 1980s and early 1990s, it became well known for releasing its source code to registered users. It allowed them to expand the code to incorporate additional software such as games or utilities and port it to other platforms. The source is now Open Source and is still updated. Confusingly WWIV has three methods of colorizing text, 10 Pipe colors, two-digit pipe colors, and its original Heart Codes.

Wildcat

WILDCAT! was a popular, propriety PC/MS-DOS application from the late 1980s that later migrated to Windows. It was one of the few BBS applications that sold at retail in a physical box. It extensively used @ color codes throughout later revisions of its software.

Example
package main

import (
	"bytes"
	"embed"
	"fmt"
	"log"

	"github.com/bengarrett/bbs"
	"golang.org/x/text/encoding/charmap"
	"golang.org/x/text/transform"
)

//go:embed static/*
var static embed.FS

func main() {
	// print about the file
	file, err := static.Open("static/examples/hello.pcb")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	s, name, err := bbs.Fields(file)
	if err != nil {
		log.Print(err)
		return
	}
	fmt.Printf("Found %d %s color controls.\n\n", len(s), name)

	// reopen the file
	file, err = static.Open("static/examples/hello.pcb")
	if err != nil {
		log.Print(err)
		return
	}
	defer file.Close()

	// transform the MS-DOS legacy text to Unicode
	decoder := charmap.CodePage437.NewDecoder()
	reader := transform.NewReader(file, decoder)

	// create the HTML equivalent of BBS color codes
	var buf bytes.Buffer
	if _, err := bbs.HTML(&buf, reader); err != nil {
		log.Print(err)
		return
	}
	fmt.Print(buf.String())

}
Output:

Found 11 PCBoard @X color controls.

<i class="PB0 PFF">    </i><i class="PB7 PF0"> ┌─────────────┐ </i><i class="PB0 PF7">
</i><i class="PB0 PFF">    </i><i class="PB7 PF0"> │ Hello </i><i class="PBF PF0">world </i><i class="PB7 PF0">│ </i><i class="PB0 PF7">
</i><i class="PB0 PFF">    </i><i class="PB7 PF0"> └─────────────┘ </i><i class="PB0 PF7"></i>

Index

Examples

Constants

View Source
const (
	CelerityRe  string = `\|(k|b|g|c|r|m|y|w|d|B|G|C|R|M|Y|W|S)` // matches Celerity
	PCBoardRe   string = "(?i)@X([0-9A-F][0-9A-F])"              // matches PCBoard
	RenegadeRe  string = `\|(0[0-9]|1[1-9]|2[0-3])`              // matches Renegade
	TelegardRe  string = "(?i)`([0-9|A-F])([0-9|A-F])"           // matches Telegard
	WildcatRe   string = `(?i)@([0-9|A-F])([0-9|A-F])@`          // matches Wildcat!
	WWIVHashRe  string = `\|#(\d)`                               // matches WWIV with hashes #
	WWIVHeartRe string = `\x03(\d)`                              // matches WWIV with hearts ♥
)

Regular expressions to match BBS color codes.

View Source
const (
	Clear string = "@CLS@"
)

Clear is a PCBoard specific control to clear the screen that's occasionally found in ANSI text.

Variables

View Source
var (
	ErrANSI = errors.New("ansi escape code found")
	ErrNone = errors.New("no bbs color code found")
)

Generic text match errors. Errors returned can be tested against these errors using errors.Is.

View Source
var (
	ErrBuff = errors.New("bytes buffer cannot be nil")
)

Syntax errors.

Functions

func CelerityHTML added in v1.0.0

func CelerityHTML(dst *bytes.Buffer, src []byte) error

CelerityHTML writes to dst the HTML equivalent of Celerity BBS color codes with matching CSS color classes.

Example
package main

import (
	"bytes"
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("|cHello |C|S|wworld")

	var buf bytes.Buffer
	if err := bbs.CelerityHTML(&buf, src); err != nil {
		fmt.Print(err)
	}
	fmt.Print(buf.String())
}
Output:

<i class="PBk PFc">Hello </i><i class="PBk PFC"></i><i class="PBw PFC">world</i>

func IsCelerity added in v1.0.0

func IsCelerity(src []byte) bool

IsCelerity reports if the bytes contains Celerity BBS color codes. The format uses the vertical bar (|) followed by a case sensitive single alphabetic character.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("|cHello |C|S|wworld")

	fmt.Print(bbs.IsCelerity(src))
}
Output:

true

func IsPCBoard added in v1.0.0

func IsPCBoard(src []byte) bool

IsPCBoard reports if the bytes contains PCBoard BBS color codes. The format uses an at-sign x (@X) prefix with a background and foreground, 4-bit hexadecimal color value.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("@X03Hello world")

	fmt.Print(bbs.IsPCBoard(src))
}
Output:

true

func IsRenegade added in v1.0.0

func IsRenegade(src []byte) bool

IsRenegade reports if the bytes contains Renegade BBS color codes. The format uses the vertical bar (|) followed by a padded, numeric value between 00 and 23.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("|03Hello |07|19world")

	fmt.Print(bbs.IsRenegade(src))
}
Output:

true

func IsTelegard added in v1.0.0

func IsTelegard(src []byte) bool

IsTelegard reports if the bytes contains Telegard BBS color codes. The format uses the grave accent (`) followed by a padded, numeric value between 00 and 23.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	const grave = "\u0060" // godoc treats a grave character as a special control
	src := []byte(grave + "7Hello world")

	fmt.Print(bbs.IsTelegard(src))
}
Output:

true

func IsWWIVHash added in v1.0.0

func IsWWIVHash(src []byte) bool

IsWWIVHash reports if the bytes contains WWIV BBS hash color codes. The format uses a vertical bar (|) with the hash (#) characters as a prefix with a numeric value between 0 and 9.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("|#7Hello world")

	fmt.Print(bbs.IsWWIVHash(src))
}
Output:

true

func IsWWIVHeart added in v1.0.0

func IsWWIVHeart(src []byte) bool

IsWWIVHeart reports if the bytes contains WWIV BBS heart (♥) color codes. The format uses the ETX (end-of-text) character as a prefix with a numeric value between 0 and 9.

In the MS-DOS era, the common North American CP-437 codepage substituted the ETX character with a heart symbol.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("\x037Hello world")

	fmt.Print(bbs.IsWWIVHeart(src))
}
Output:

true

func IsWildcat added in v1.0.0

func IsWildcat(src []byte) bool

IsWildcat reports if the bytes contains Wildcat! BBS color codes. The format uses an a background and foreground, 4-bit hexadecimal color value enclosed with two at-sign (@) characters.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("@0F@Hello world")

	fmt.Print(bbs.IsWildcat(src))
}
Output:

true

func PCBoardHTML added in v1.0.0

func PCBoardHTML(dst *bytes.Buffer, src []byte) error

PCBoardHTML writes to dst the HTML equivalent of PCBoard BBS color codes with matching CSS color classes.

Example
package main

import (
	"bytes"
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("@X03Hello world")

	var buf bytes.Buffer
	if err := bbs.PCBoardHTML(&buf, src); err != nil {
		fmt.Print(err)
	}
	fmt.Print(buf.String())
}
Output:

<i class="PB0 PF3">Hello world</i>

func RenegadeHTML added in v1.0.0

func RenegadeHTML(dst *bytes.Buffer, src []byte) error

RenegadeHTML writes to dst the HTML equivalent of Renegade BBS color codes with matching CSS color classes.

Example
package main

import (
	"bytes"
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("|03Hello |07|19world")

	var buf bytes.Buffer
	if err := bbs.RenegadeHTML(&buf, src); err != nil {
		fmt.Print(err)
	}
	fmt.Print(buf.String())
}
Output:

<i class="P0 P3">Hello </i><i class="P0 P7"></i><i class="P19 P7">world</i>

func TelegardHTML added in v1.0.0

func TelegardHTML(dst *bytes.Buffer, src []byte) error

TelegardHTML writes to dst the HTML equivalent of Telegard BBS color codes with matching CSS color classes.

func TrimControls

func TrimControls(src []byte) []byte

TrimControls removes common PCBoard BBS controls prefixes from the bytes. It trims the @CLS@ prefix used to clear the screen and the @PAUSE@ prefix used to pause the display render.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("@CLS@@PAUSE@Hello world")

	fmt.Printf("%q trims to %s", src, string(bbs.TrimControls(src)))
}
Output:

"@CLS@@PAUSE@Hello world" trims to Hello world

func WWIVHashHTML added in v1.0.0

func WWIVHashHTML(dst *bytes.Buffer, src []byte) error

WWIVHashHTML writes to dst the HTML equivalent of WWIV BBS hash (#) color codes with matching CSS color classes.

func WWIVHeartHTML added in v1.0.0

func WWIVHeartHTML(dst *bytes.Buffer, src []byte) error

WWIVHeartHTML writes to dst the HTML equivalent of WWIV BBS heart (♥) color codes with matching CSS color classes.

func WildcatHTML added in v1.0.0

func WildcatHTML(dst *bytes.Buffer, src []byte) error

WildcatHTML writes to dst the HTML equivalent of Wildcat! BBS color codes with matching CSS color classes.

Types

type BBS

type BBS int

A BBS (Bulletin Board System) color code format, other than for Find, the ANSI BBS is not supported by this library.

const (
	ANSI      BBS = iota // ANSI escape sequence.
	Celerity             // Celerity pipe.
	PCBoard              // PCBoard @ sign.
	Renegade             // Renegade pipe.
	Telegard             // Telegard grave accent.
	Wildcat              // Wildcat! @ sign.
	WWIVHash             // WWIV # symbol.
	WWIVHeart            // WWIV ♥ symbol.
)

BBS codes and sequences.

func Fields

func Fields(src io.Reader) ([]string, BBS, error)

Fields splits the io.Reader around the first instance of one or more consecutive BBS color codes. An error is returned if no color codes are found or if ANSI control sequences are first found.

Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	r := strings.NewReader("@X03Hello @XF0world")

	s, b, err := bbs.Fields(r)
	if err != nil {
		log.Print(err)
	}

	fmt.Printf("Found %d, %s sequences\n", len(s), b)
	for i, item := range s {
		fmt.Printf("Sequence %d: %q\n", i+1, item)
	}
}
Output:

Found 2, PCBoard @X sequences
Sequence 1: "03Hello "
Sequence 2: "F0world"
Example (Ansi)
package main

import (
	"errors"
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	const reset = "\x1b[0m" // an ANSI escape sequence to reset the terminal
	r := strings.NewReader(reset + "Hello world")

	s, b, err := bbs.Fields(r)
	if errors.Is(err, bbs.ErrANSI) {
		fmt.Printf("error: %s", err)
		return
	}
	fmt.Printf("Found %d, %s sequences\n", len(s), b)
}
Output:

error: ansi escape code found
Example (None)
package main

import (
	"errors"
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	r := strings.NewReader("Hello world")

	s, b, err := bbs.Fields(r)
	if errors.Is(err, bbs.ErrNone) {
		fmt.Printf("error: %s", err)
		return
	}
	fmt.Printf("Found %d, %s sequences\n", len(s), b)
}
Output:

error: no bbs color code found

func Find

func Find(src io.Reader) BBS

Find the format of any known BBS color code sequence within the reader. If no sequences are found -1 is returned.

Example
package main

import (
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	src := strings.NewReader("@X03Hello world")

	f := bbs.Find(src)
	fmt.Printf("Found %s text", f.Name())
}
Output:

Found PCBoard text
Example (Ansi)
package main

import (
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	const reset = "\x1b[0m" // an ANSI escape sequence to reset the terminal
	src := strings.NewReader(reset + "Hello world")

	f := bbs.Find(src)
	if !f.Valid() {
		fmt.Print("Found plain text")
		return
	}
	fmt.Printf("Found %s text", f.Name())
}
Output:

Found ANSI text
Example (None)
package main

import (
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	src := strings.NewReader("Hello world")

	f := bbs.Find(src)
	if !f.Valid() {
		fmt.Print("Found plain text")
		return
	}
	fmt.Printf("Found %s text", f.Name())
}
Output:

Found plain text

func HTML

func HTML(dst *bytes.Buffer, src io.Reader) (BBS, error)

HTML writes to dst the HTML equivalent of BBS color codes with matching CSS color classes. The first found color code format is used for the remainder of the Reader.

Example
package main

import (
	"bytes"
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	src := strings.NewReader("@X03Hello world")

	var buf bytes.Buffer
	r, err := bbs.HTML(&buf, src)
	if err != nil {
		fmt.Print(err)
		return
	}

	fmt.Printf("<!-- %s code -->\n", r)
	fmt.Print(buf.String())
}
Output:

<!-- PCBoard @X code -->
<i class="PB0 PF3">Hello world</i>

func (BBS) Bytes

func (b BBS) Bytes() []byte

Bytes returns the BBS color toggle sequence.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	b := bbs.PCBoard.Bytes()
	fmt.Printf("Code as bytes %v\n", b)
	fmt.Printf("Code as string %s", b)
}
Output:

Code as bytes [64 88]
Code as string @X

func (BBS) CSS

func (b BBS) CSS(dst *bytes.Buffer) error

CSS writes to dst the Cascading Style Sheets classes needed by the HTML.

The CSS results rely on custom properties which are not supported by legacy browsers.

Example
package main

import (
	"bytes"
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	var css bytes.Buffer
	if err := bbs.PCBoard.CSS(&css); err != nil {
		fmt.Print(err)
	}
	// print the first 8 lines of the css
	lines := strings.Split(css.String(), "\n")
	for i := 0; i < 8; i++ {
		fmt.Println(lines[i])
	}
}
Output:

@import url("text_bbs.css");
@import url("text_blink.css");

/* PCBoard and WildCat! BBS colours */

i.PF0 {
    color: var(--black);
}

func (BBS) HTML

func (b BBS) HTML(dst *bytes.Buffer, src []byte) error

HTML writes to dst the BBS color codes as CSS color classes within HTML <i> elements.

Example
package main

import (
	"bytes"
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("@X03Hello @X04world@X00")

	var buf bytes.Buffer
	if err := bbs.PCBoard.HTML(&buf, src); err != nil {
		fmt.Print(err)
		return
	}
	fmt.Print(buf.String())
}
Output:

<i class="PB0 PF3">Hello </i><i class="PB0 PF4">world</i><i class="PB0 PF0"></i>
Example (Ansi)
package main

import (
	"bytes"
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	const reset = "\x1b[0m" // an ANSI escape sequence to reset the terminal
	src := []byte(reset + "Hello world")

	result := bbs.Find(bytes.NewReader(src))

	var buf bytes.Buffer
	if err := result.HTML(&buf, src); err != nil {
		fmt.Printf("error: %s", err)
		return
	}
	fmt.Print(buf.String())
}
Output:

error: ansi escape code found
Example (Find)
package main

import (
	"bytes"
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("@X03Hello @X04world@X00")

	result := bbs.Find(bytes.NewReader(src))

	var buf bytes.Buffer
	if err := result.HTML(&buf, src); err != nil {
		fmt.Print(err)
		return
	}
	fmt.Print(buf.String())
}
Output:

<i class="PB0 PF3">Hello </i><i class="PB0 PF4">world</i><i class="PB0 PF0"></i>

func (BBS) Name

func (b BBS) Name() string

Name returns the name of the BBS color format.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	fmt.Print(bbs.PCBoard.Name())
}
Output:

PCBoard

func (BBS) Remove

func (b BBS) Remove(dst *bytes.Buffer, src []byte) error

Remove the BBS color codes from src and write it to dst.

Example
package main

import (
	"bytes"
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("@X03Hello @X07world")

	var buf bytes.Buffer
	if err := bbs.PCBoard.Remove(&buf, src); err != nil {
		fmt.Print(err)
	}
	fmt.Printf("%q to %s", src, buf.String())
}
Output:

"@X03Hello @X07world" to Hello world
Example (Find)
package main

import (
	"bytes"
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	src := []byte("@X03Hello @X07world")

	result := bbs.Find(bytes.NewReader(src))

	var buf bytes.Buffer
	if err := result.Remove(&buf, src); err != nil {
		fmt.Print(err)
	}
	fmt.Printf("%q to %s", src, buf.String())
}
Output:

"@X03Hello @X07world" to Hello world

func (BBS) String

func (b BBS) String() string

String returns the BBS color format name and toggle sequence.

Example
package main

import (
	"fmt"

	"github.com/bengarrett/bbs"
)

func main() {
	fmt.Print(bbs.PCBoard)
}
Output:

PCBoard @X

func (BBS) Valid

func (b BBS) Valid() bool

Valid reports whether the BBS type is valid.

Example
package main

import (
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	src := "@X03Hello @X07world"

	r := strings.NewReader(src)
	ok := bbs.Find(r).Valid()
	fmt.Print(ok)
}
Output:

true
Example (Ansi)
package main

import (
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	const reset = "\x1b[0m" // an ANSI escape sequence to reset the terminal
	src := reset + "Hello world"

	r := strings.NewReader(src)
	ok := bbs.Find(r).Valid()
	fmt.Print(ok)
}
Output:

true
Example (False)
package main

import (
	"fmt"
	"strings"

	"github.com/bengarrett/bbs"
)

func main() {
	src := "Hello world"

	r := strings.NewReader(src)
	ok := bbs.Find(r).Valid()
	fmt.Print(ok)
}
Output:

false

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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