rxp

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2024 License: Apache-2.0 Imports: 6 Imported by: 15

README

godoc codecov Go Report Card

rxp

rxp is an experiment in doing regexp-like things, without actually using regexp to do any of the work.

For most use cases, the regexp package is likely the correct choice as it is fairly optimized and uses the familiar regular expression byte/string patterns to compile and use to match and replace text.

rxp by contrast doesn't really have a compilation phase, rather it is simply the declaration of a Pattern, which is simply a slice of Matcher functions, and to do the neat things one needs to do with regular expressions, simply use the methods on the Pattern list.

Notice

This is the v0.2.x series with lots of scaffolding still present (stuff devs write just to get things working before refining and optimizing work).

This version is being published for the purpose of documenting the evolution of rxp over time.

Installation

> go get github.com/go-corelibs/rxp@latest

Examples

Find all words at the start of any line of input

// regexp version:
m := regexp.
    MustCompile(`(?:m)^\s*(\w+)\b`).
    FindAllStringSubmatch(input, -1)

// equivalent rxp version
m := rxp.Pattern{}.
    Caret("m").S("*").W("+", "c").B().
    FindAllStringSubmatch(input, -1)

Perform a series of text transformations

For whatever reason, some text needs to be transformed and these transformations must satisfy four requirements: lowercase everything, consecutive spaces become one space, single quotes must be turned into underscores and all non-alphanumeric-underscore-or-spaces be removed.

These requirements can be explored with the traditional Perl substitution syntax, as in the following table:

# Perl Expression Description
1 s/[A-Z]+/\L${1}\e/mg lowercase all letters
2 s/\s+/ /mg collapse all spaces
3 s/[']/_/mg single quotes to underscores
4 s/[^\w\s]+//mg delete non-word-or-spaces

The result of the above should take: Isn't this neat? and transform it into: isn_t this neat.

// using regexp:
output := strings.ToLower(`Isn't  this  neat?`)
output = regexp.MustCompile(`\s+`).ReplaceAllString(output, " ")
output = regexp.MustCompile(`[']`).ReplaceAllString(output, "_")
output = regexp.MustCompile(`[^\w ]`).ReplaceAllString(output, "")

// using rxp:
output := rxp.Pipeline{}.
	Transform(strings.ToLower).
	ReplaceText(rxp.S("+"), " ").
	ReplaceText(rxp.Text("'"), "_").
	ReplaceText(rxp.Not(rxp.Or(rxp.W(), rxp.S()), "c"), "").
	Process(`Isn't  this  neat?`)

Go-CoreLibs

Go-CoreLibs is a repository of shared code between the Go-Curses and Go-Enjin projects.

License

Copyright 2024 The Go-CoreLibs Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use file except in compliance with the License.
You may obtain a copy of the license at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Documentation

Overview

Package rxp is an experiment in doing regexp-like things, without actually using regexp to do the work

Index

Constants

This section is empty.

Variables

View Source
var DefaultReps = Reps{1, 1}

Functions

func ParseFlags

func ParseFlags(flags ...string) (Reps, Flags)

ParseFlags parses a regexp-like option string into a Flags instance and two integers, the low and high range of repetitions

|  Flags  | Description                                                                             |
|---------|-----------------------------------------------------------------------------------------|
|    ^    | Invert the meaning of this match group                                                  |
|    m    | Multiline mode Caret and Dollar match begin/end of line in addition to begin/end text   |
|    s    | DotNL allows Dot to match newlines (\n)                                                 |
|    i    | AnyCase is case-insensitive matching of unicode text                                    |
|    c    | Capture allows this Matcher to be included in Pattern substring results                 |
|    *    | zero or more repetitions, prefer more                                                   |
|    +    | one or more repetitions, prefer more                                                    |
|    ?    | zero or one repetition, prefer one                                                      |
|  {l,h}  | range of repetitions, l minimum and up to h maximum, prefer more                        |
|  {l,}   | range of repetitions, l minimum, prefer more                                            |
|  {l}    | range of repetitions, l minimum, prefer more                                            |
|   *?    | zero or more repetitions, prefer less                                                   |
|   +?    | one or more repetitions, prefer less                                                    |
|   ??    | zero or one repetition, prefer zero                                                     |
|  {l,h}? | range of repetitions, l minimum and up to h maximum, prefer less                        |
|  {l,}?  | range of repetitions, l minimum, prefer less                                            |
|  {l}?   | range of repetitions, l minimum, prefer less                                            |

The flags presented above can be combined into a single string argument, or can be individually given to ParseFlags

Any parsing errors will result in a runtime panic

func RuneIsALNUM

func RuneIsALNUM(r rune) bool

RuneIsALNUM returns true for alphanumeric characters [a-zA-Z0-9]

func RuneIsALPHA

func RuneIsALPHA(r rune) bool

RuneIsALPHA returns true for alphanumeric characters [a-zA-Z]

func RuneIsASCII

func RuneIsASCII(r rune) bool

RuneIsASCII returns true for valid ASCII characters [\x00-\x7F]

func RuneIsBLANK

func RuneIsBLANK(r rune) bool

RuneIsBLANK returns true for tab and space characters [\t ]

func RuneIsCNTRL

func RuneIsCNTRL(r rune) bool

RuneIsCNTRL returns true for control characters [\x00-\x1F\x7F]

func RuneIsDIGIT

func RuneIsDIGIT(r rune) bool

RuneIsDIGIT returns true for number digits [0-9]

func RuneIsDashUnder

func RuneIsDashUnder(r rune) bool

func RuneIsDashUnderALNUM

func RuneIsDashUnderALNUM(r rune) bool

func RuneIsGRAPH

func RuneIsGRAPH(r rune) bool

RuneIsGRAPH returns true for graphical characters [a-zA-Z0-9!"$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]

Note: upon the first use of RuneIsGRAPH, a lookup map is cached in a global variable and used for detecting the specific runes supported by the regexp [:graph:] class

func RuneIsLOWER

func RuneIsLOWER(r rune) bool

RuneIsLOWER returns true for lowercase alphabetic characters [a-z]

func RuneIsPRINT

func RuneIsPRINT(r rune) bool

RuneIsPRINT returns true for space and RuneIsGRAPH characters [ [:graph:]]

Note: uses RuneIsGRAPH

func RuneIsPUNCT

func RuneIsPUNCT(r rune) bool

RuneIsPUNCT returns true for punctuation characters [!-/:-@[-`{-~]

func RuneIsPlusMinus

func RuneIsPlusMinus(r rune) bool

func RuneIsSPACE

func RuneIsSPACE(r rune) bool

RuneIsSPACE returns true for empty space characters [\t\n\v\f\r ]

func RuneIsSpace

func RuneIsSpace(r rune) bool

RuneIsSpace returns true for space characters [\t\n\f\r ]

func RuneIsUPPER

func RuneIsUPPER(r rune) bool

RuneIsUPPER returns true for lowercase alphabetic characters [A-Z]

func RuneIsWord

func RuneIsWord(r rune) bool

RuneIsWord returns true for word characters [_a-zA-Z0-9]

func RuneIsXDIGIT

func RuneIsXDIGIT(r rune) bool

RuneIsXDIGIT returns true for hexadecimal digits [z-fA-F0-9]

Types

type AsciiNames

type AsciiNames string
const (
	ALNUM  AsciiNames = "alnum"
	ALPHA  AsciiNames = "alpha"
	ASCII  AsciiNames = "ascii"
	BLANK  AsciiNames = "blank"
	CNTRL  AsciiNames = "cntrl"
	DIGIT  AsciiNames = "digit"
	GRAPH  AsciiNames = "graph"
	LOWER  AsciiNames = "lower"
	PRINT  AsciiNames = "print"
	PUNCT  AsciiNames = "punct"
	SPACE  AsciiNames = "space"
	UPPER  AsciiNames = "upper"
	WORD   AsciiNames = "word"
	XDIGIT AsciiNames = "xdigit"
)

type Flags added in v0.2.0

type Flags interface {
	Negated() bool
	Multiline() bool
	DotNL() bool
	AnyCase() bool
	Capture() bool
	Less() bool

	SetNegated()
	SetCapture()

	Clone() Flags
	Merge(other Flags)
	Equal(other Flags) bool
	String() string
	// contains filtered or unexported methods
}

type Fragment

type Fragment interface {
	// Match returns the associated Match for this Fragment, if this
	// Fragment is for matched text
	Match() (result Match, ok bool)
	// Index returns the start and end indexes of this Fragment
	Index() []int
	// Runes returns the input runes for this Index range
	Runes() []rune
	// String is a convenience wrapper around Runes
	String() string
	// Bytes is a convenience wrapper around Runes
	Bytes() []byte

	Recycle()
	// contains filtered or unexported methods
}

Fragment is the Pattern.ScanString return value and can be either a Result of a captured Matcher or unmatched plain text. The Fragment slice returned by Pattern.ScanString can be used to rebuild the original input

type Fragments

type Fragments []Fragment

func (Fragments) Indexes

func (f Fragments) Indexes() (indexes [][]int)

func (Fragments) Recycle

func (f Fragments) Recycle()

func (Fragments) String

func (f Fragments) String() string

func (Fragments) Strings

func (f Fragments) Strings() (found []string)

type IMatchState

type IMatchState interface {
	// Input returns the complete input rune slice
	Input() []rune
	// InputLen returns the total number of input runes (not bytes)
	InputLen() int
	// Index returns the start position for this Matcher, use Index + Len to
	// get the correct position within Input for This rune
	Index() int
	// Len is count of runes consumed so far by this Matcher
	Len() int
	// End returns true if this MatchState position (index+len) is exactly the
	// InputLen, denoting the Dollar zero-width position (End Of Input)
	End() bool
	// Ready returns true if this MatchState position (index+len) is less than
	// the InputLen (Ready is before End, End is Valid, past End is Invalid)
	Ready() bool
	// Valid returns true if this MatchState position (index+len) is less than
	// or equal to the InputLen (End is Valid, past the End is Invalid)
	Valid() bool
	// Invalid returns true if this MatchState position (index+len) is greater
	// than to the InputLen (End is Valid, past the End is Invalid)
	Invalid() bool
	// Has returns true if the given index is an input rune
	Has(idx int) (ok bool)
	// Get returns the input rune at the given index, if there is one
	Get(idx int) (r rune, ok bool)
	// Prev returns the previous rune, if there is one
	Prev() (r rune, ok bool)
	// This is the current rune being considered for this Matcher, if there is one
	This() (r rune, ok bool)
	// Next peeks at the next rune, if there is one
	Next() (r rune, ok bool)

	Reps() Reps

	// Captured returns true if Capture was previously called on this MatchState
	Captured() bool

	// Flags returns a clone Flags
	Flags() Flags

	Runes() []rune
	String() (text string)

	// Equal returns true if the other MatchState is the same as this one
	Equal(other MatchState) bool
}

type Match

type Match interface {
	// Runes returns teh complete text for this Pattern match as a rune slice
	Runes() []rune
	// String returns the complete text for this Pattern match
	String() (complete string)
	// Submatch returns the specific value of the capture group at the given
	// index, if it exists
	Submatch(idx int) (value string, ok bool)
	// contains filtered or unexported methods
}

type MatchState

type MatchState interface {
	IMatchState

	// Capture includes this Matcher in any sub-matching results
	Capture()

	// Consume associates the current rune with this match and moves the MatchState
	// forward by count runes, Keep returns true if there are still more runes
	// remaining to process
	Consume(count int) (ok bool)

	// Clone is a wrapper around a zero offset call to CloneWith
	Clone() MatchState

	// CloneWith returns an exact copy of this MatchState with the start point
	// offset by the positive amount given
	CloneWith(offset int, reps Reps) MatchState

	// Apply updates the other MatchState with this MatchState consumed runes,
	// capture state and Config Scope
	Apply(other MatchState)

	// Scope append (if the cfg is not nil) a new Config scope within this
	// matcher and always returns a composite Config of all scoped configs
	Scope(reps Reps, other Flags) (lh Reps, scope Flags)

	Recycle()
	// contains filtered or unexported methods
}

MatchState is used by Matcher functions to progress the Pattern matching process

type Matcher

type Matcher func(m MatchState) (next, keep bool)

Matcher is a single string matching function

next  indicates that this Matcher function does not match the current rune
      and to progress the Pattern to the next Matcher instance
keep  indicates that this Matcher function was in fact satisfied, even
      though stop may also be true

func A

func A() Matcher

A creates a Matcher equivalent to the regexp [\A]

func Alnum

func Alnum(flags ...string) Matcher

Alnum creates a Matcher equivalent to [:alnum:]

func Alpha

func Alpha(flags ...string) Matcher

Alpha creates a Matcher equivalent to [:alpha:]

func Ascii

func Ascii(flags ...string) Matcher

Ascii creates a Matcher equivalent to [:ascii:]

func B

func B() Matcher

B creates a Matcher equivalent to the regexp [\b]

func Blank

func Blank(flags ...string) Matcher

Blank creates a Matcher equivalent to [:blank:]

func Caret

func Caret(options ...string) Matcher

Caret creates a Matcher equivalent to the regexp caret [^]

func Class

func Class(name AsciiNames, flags ...string) Matcher

Class creates a Matcher equivalent to the regexp [:AsciiNames:], see the AsciiNames constants for the list of supported ASCII class names

Class will panic if given an invalid class name

func Cntrl

func Cntrl(flags ...string) Matcher

Cntrl creates a Matcher equivalent to [:cntrl:]

func D

func D(flags ...string) Matcher

D creates a Matcher equivalent to the regexp \d

func Digit

func Digit(flags ...string) Matcher

Digit creates a Matcher equivalent to [:digit:]

func Dollar

func Dollar(options ...string) Matcher

Dollar creates a Matcher equivalent to the regexp [$]

func Dot

func Dot(flags ...string) Matcher

Dot creates a Matcher equivalent to the regexp dot (.)

func FieldKey

func FieldKey(flags ...string) Matcher

FieldKey creates a Matcher equivalent to:

(?:\b[a-zA-Z][-_a-zA-Z0-9]+?[a-zA-Z0-9]\b)

func FieldWord

func FieldWord(flags ...string) Matcher

FieldWord creates a Matcher equivalent to:

(?:\b[a-zA-Z0-9]+?['a-zA-Z0-9]*[a-zA-Z0-9]+\b|\b[a-zA-Z0-9]+\b)

func Graph

func Graph(flags ...string) Matcher

Graph creates a Matcher equivalent to [:graph:]

func Group

func Group(options ...interface{}) Matcher

Group processes the list of Matcher instances, in the order they were given, and stops at the first one that does not match, discarding any consumed runes. If all Matcher calls succeed, all consumed runes are applied

func IsUnicodeRange

func IsUnicodeRange(table *unicode.RangeTable, flags ...string) Matcher

IsUnicodeRange creates a Matcher equivalent to the regexp \pN where N is a unicode character class, passed to IsUnicodeRange as a unicode.RangeTable instance

For example, creating a Matcher for a single braille character:

IsUnicodeRange(unicode.Braille)

func Keyword

func Keyword(flags ...string) Matcher

Keyword is intended for Go-Enjin parsing of simple search keywords from user input and creates a Matcher equivalent to:

(?:\b[-+]?[a-zA-Z][-_a-zA-Z0-9]+?[a-zA-Z0-9]\b)

func Lower

func Lower(flags ...string) Matcher

Lower creates a Matcher equivalent to [:lower:]

func MakeRuneMatcher

func MakeRuneMatcher(match RuneMatcher, flags ...string) Matcher

MakeRuneMatcher creates a rxp standard Matcher implementation wrapped around a given RuneMatcher

func Not

func Not(options ...interface{}) Matcher

Not processes the given Matcher and inverts the next response without consuming any runes (zero-width Matcher)

Not accepts Pattern, Matcher and string types and will panic on all others

func Or

func Or(options ...interface{}) Matcher

Or processes the list of Matcher instances, in the order they were given, and stops at the first one that returns a true next

Or accepts Pattern, Matcher and string types and will panic on all others

func Print

func Print(flags ...string) Matcher

Print creates a Matcher equivalent to [:print:]

func Punct

func Punct(flags ...string) Matcher

Punct creates a Matcher equivalent to [:punct:]

func S

func S(flags ...string) Matcher

S creates a Matcher equivalent to the regexp \s

func Space

func Space(flags ...string) Matcher

Space creates a Matcher equivalent to [:space:]

func Text

func Text(text string, flags ...string) Matcher

Text creates a Matcher for the plain text given

func Upper

func Upper(flags ...string) Matcher

Upper creates a Matcher equivalent to [:upper:]

func W

func W(flags ...string) Matcher

W creates a Matcher equivalent to the regexp \w

func Word

func Word(flags ...string) Matcher

Word creates a Matcher equivalent to [:word:]

func WrapFn

func WrapFn(matcher RuneMatchFn, flags ...string) Matcher

WrapFn wraps a RuneMatchFn with MakeRuneMatcher

func Xdigit

func Xdigit(flags ...string) Matcher

Xdigit creates a Matcher equivalent to [:xdigit:]

func Z

func Z() Matcher

Z is a Matcher equivalent to the regexp [\z]

type Pattern

type Pattern []Matcher

Pattern is a list of Matcher functions, all of which must match, in the order present, in order to consider the Pattern to match

func ParseOptions

func ParseOptions(options ...interface{}) (pattern Pattern, flags []string, argv []interface{})

ParseOptions accepts Pattern, Matcher and string options and recasts them into their specific types

ParseOptions will panic with any type other than Pattern, Matcher or string

func (Pattern) A

func (p Pattern) A() Pattern

func (Pattern) Add

func (p Pattern) Add(matcher Matcher) Pattern

func (Pattern) Alnum

func (p Pattern) Alnum(flags ...string) Pattern

func (Pattern) Alpha

func (p Pattern) Alpha(flags ...string) Pattern

func (Pattern) Ascii

func (p Pattern) Ascii(flags ...string) Pattern

func (Pattern) B

func (p Pattern) B() Pattern

func (Pattern) Blank

func (p Pattern) Blank(flags ...string) Pattern

func (Pattern) Caret

func (p Pattern) Caret(flags ...string) Pattern

func (Pattern) Class

func (p Pattern) Class(name AsciiNames, flags ...string) Pattern

func (Pattern) Cntrl

func (p Pattern) Cntrl(flags ...string) Pattern

func (Pattern) D

func (p Pattern) D(flags ...string) Pattern

func (Pattern) Digit

func (p Pattern) Digit(flags ...string) Pattern

func (Pattern) Dollar

func (p Pattern) Dollar(flags ...string) Pattern

func (Pattern) Dot

func (p Pattern) Dot(flags ...string) Pattern

func (Pattern) FindAllString

func (p Pattern) FindAllString(input string, count int) (found []string)

func (Pattern) FindAllStringIndex

func (p Pattern) FindAllStringIndex(input string, count int) (found [][]int)

func (Pattern) FindAllStringSubmatch

func (p Pattern) FindAllStringSubmatch(input string, count int) (found [][]string)

func (Pattern) FindIndex

func (p Pattern) FindIndex(input string) (found []int)

func (Pattern) FindString

func (p Pattern) FindString(input string) string

func (Pattern) FindStringSubmatch

func (p Pattern) FindStringSubmatch(input string) (found []string)

func (Pattern) Graph

func (p Pattern) Graph(flags ...string) Pattern

func (Pattern) Group

func (p Pattern) Group(options ...interface{}) Pattern

func (Pattern) Lower

func (p Pattern) Lower(flags ...string) Pattern

func (Pattern) Match

func (p Pattern) Match(input []rune) (ok bool)

func (Pattern) MatchString

func (p Pattern) MatchString(input string) (ok bool)

func (Pattern) Not

func (p Pattern) Not(options ...interface{}) Pattern

func (Pattern) Or

func (p Pattern) Or(options ...interface{}) Pattern

func (Pattern) Print

func (p Pattern) Print(flags ...string) Pattern

func (Pattern) Punct

func (p Pattern) Punct(flags ...string) Pattern

func (Pattern) Range

func (p Pattern) Range(table *unicode.RangeTable, flags ...string) Pattern

func (Pattern) ReplaceAllString

func (p Pattern) ReplaceAllString(input string, repl Replace) string

func (Pattern) ReplaceAllStringFunc

func (p Pattern) ReplaceAllStringFunc(input string, repl Transform) string

func (Pattern) S

func (p Pattern) S(flags ...string) Pattern

func (Pattern) ScanString

func (p Pattern) ScanString(input string) (fragments Fragments)

func (Pattern) Space

func (p Pattern) Space(flags ...string) Pattern

func (Pattern) Text

func (p Pattern) Text(text string, flags ...string) Pattern

func (Pattern) Upper

func (p Pattern) Upper(flags ...string) Pattern

func (Pattern) W

func (p Pattern) W(flags ...string) Pattern

func (Pattern) Word

func (p Pattern) Word(flags ...string) Pattern

func (Pattern) Xdigit

func (p Pattern) Xdigit(flags ...string) Pattern

func (Pattern) Z

func (p Pattern) Z() Pattern

type Pipeline

type Pipeline []Stage

Pipeline is a list of stages for transforming text in a single procedure

Pipeline is also a pseudo-buildable thing using the Transform, Replace, ReplaceText and ReplaceWith methods which return the updated Pipeline

func (Pipeline) Process

func (p Pipeline) Process(input string) (output string)

Process returns the output of a complete Pipeline transformation of the input string, and is obviously not a buildable method as it returns a string instead of an updated Pipeline

func (Pipeline) Replace

func (p Pipeline) Replace(search interface{}, replace Replace) Pipeline

func (Pipeline) ReplaceText

func (p Pipeline) ReplaceText(search interface{}, text string) Pipeline

func (Pipeline) ReplaceWith

func (p Pipeline) ReplaceWith(search interface{}, transform Transform) Pipeline

func (Pipeline) Transform

func (p Pipeline) Transform(transform Transform) Pipeline

type Replace

type Replace []Replacement

Replace is a Replacement pipeline

func (Replace) ToLower

func (r Replace) ToLower() Replace

ToLower is a convenience wrapper around WithTransform and strings.ToLower

func (Replace) ToUpper

func (r Replace) ToUpper() Replace

ToUpper is a convenience wrapper around WithTransform and strings.ToUpper

func (Replace) WithReplace

func (r Replace) WithReplace(replacer Replacement) Replace

WithReplace replaces matches using a Replacement function

func (Replace) WithText

func (r Replace) WithText(text string) Replace

WithText replaces matches with the plain text given

func (Replace) WithTransform

func (r Replace) WithTransform(transform Transform) Replace

WithTransform replaces matches with a Transform function

type Replacement

type Replacement func(s Match) (replaced string)

Replacement is a Match.String replacement function

type Reps added in v0.2.0

type Reps []int

func (Reps) Max added in v0.2.0

func (r Reps) Max() int

func (Reps) Min added in v0.2.0

func (r Reps) Min() int

type RuneMatchFn

type RuneMatchFn func(r rune) bool

RuneMatchFn is the signature for the basic character matching functions such as RuneIsWord

Implementations are expected to operate using the least amount of CPU instructions possible

type RuneMatcher

type RuneMatcher func(cfg Flags, m MatchState, pos int, r rune) (consumed int, proceed bool)

RuneMatcher is the signature for the MakeRuneMatcher matching function

The MatchState provided to RuneMatcher functions is a MatchState.Clone and any changes to that MatchState by the RuneMatcher are discarded. In order to have the RuneMatcher consume any runes, return the consumed number of runes and a true proceed

type Stage

type Stage struct {
	// Search is a Pattern of text, used with Replace to modify the matching
	// text
	Search    Pattern
	Replace   Replace
	Transform Transform
}

Stage is one phase of a text replacement Pipeline and receives an input string from the previous stage (or the initial input text) and returns the output provided to the next stage (or is finally returned to the caller)

func (Stage) Process

func (s Stage) Process(input string) (output string)

Process is the Pipeline processor method for a Stage

If there is a Search Pattern present, and there is at least one Replacement functions present, then the process returns a Search Pattern.ReplaceAllString

If there is a Search Pattern present, and there are no Replacement functions present, then the process returns a Search Pattern.ReplaceAllStringFunc

type SyncPool

type SyncPool[V interface{}] interface {
	Scale() int
	Ready() int
	Seed(count int)
	Get() V
	Put(v V)
}

type Transform

type Transform func(input string) (output string)

Transform is the function signature for non-rxp string transformation pipeline stages

Jump to

Keyboard shortcuts

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