Initial lookbook implementation
Pinterest-style visual bookmarking app with: - URL metadata extraction (OG/Twitter meta, oEmbed fallback) - Image caching in Postgres with 480px thumbnails - Multi-tag filtering with Ctrl/Cmd for OR mode - Fuzzy tag suggestions and inline tag editing - Browser console auth() with first-use password setup - Brutalist UI with Commit Mono font and Pico CSS - Light/dark mode via browser preference
This commit is contained in:
commit
fc625fb9cf
486 changed files with 195373 additions and 0 deletions
24
vendor/github.com/mfridman/interpolate/LICENSE.txt
generated
vendored
Normal file
24
vendor/github.com/mfridman/interpolate/LICENSE.txt
generated
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2014-2017 Buildkite Pty Ltd
|
||||
Copyright (c) 2023 Michael Fridman
|
||||
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
85
vendor/github.com/mfridman/interpolate/README.md
generated
vendored
Normal file
85
vendor/github.com/mfridman/interpolate/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Interpolate
|
||||
|
||||
[](https://github.com/mfridman/interpolate/actions/workflows/ci.yaml)
|
||||
[](https://pkg.go.dev/github.com/mfridman/interpolate)
|
||||
[](https://goreportcard.com/report/github.com/mfridman/interpolate)
|
||||
|
||||
A Go library for parameter expansion (like `${NAME}` or `$NAME`) in strings from environment
|
||||
variables. An implementation of [POSIX Parameter
|
||||
Expansion](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02),
|
||||
plus some other basic operations that you'd expect in a shell scripting environment [like
|
||||
bash](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/mfridman/interpolate@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mfridman/interpolate"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
env := interpolate.NewSliceEnv([]string{
|
||||
"NAME=James",
|
||||
})
|
||||
|
||||
output, _ := interpolate.Interpolate(env, "Hello... ${NAME} welcome to the ${ANOTHER_VAR:-🏖}")
|
||||
|
||||
fmt.Println(output)
|
||||
// Output: Hello... James welcome to the 🏖
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Expansions
|
||||
|
||||
- `${parameter}` or `$parameter`
|
||||
|
||||
- **Use value.** If parameter is set, then it shall be substituted; otherwise, it will be blank
|
||||
|
||||
- `${parameter:-[word]}`
|
||||
|
||||
- **Use default values.** If parameter is unset or null, the expansion of word (or an empty string
|
||||
if word is omitted) shall be substituted; otherwise, the value of parameter shall be
|
||||
substituted.
|
||||
|
||||
- `${parameter-[word]}`
|
||||
|
||||
- **Use default values when not set.** If parameter is unset, the expansion of word (or an empty
|
||||
string if word is omitted) shall be substituted; otherwise, the value of parameter shall be
|
||||
substituted.
|
||||
|
||||
- `${parameter:[offset]}`
|
||||
|
||||
- **Use the substring of parameter after offset.** A negative offset must be separated from the
|
||||
colon with a space, and will select from the end of the string. If the value is out of bounds,
|
||||
an empty string will be substituted.
|
||||
|
||||
- `${parameter:[offset]:[length]}`
|
||||
|
||||
- **Use the substring of parameter after offset of given length.** A negative offset must be
|
||||
separated from the colon with a space, and will select from the end of the string. If the offset
|
||||
is out of bounds, an empty string will be substituted. If the length is greater than the length
|
||||
then the entire string will be returned.
|
||||
|
||||
- `${parameter:?[word]}`
|
||||
- **Indicate Error if Null or Unset.** If parameter is unset or null, the expansion of word (or a
|
||||
message indicating it is unset if word is omitted) shall be returned as an error.
|
||||
|
||||
## Prior work
|
||||
|
||||
This repository is a fork of [buildkite/interpolate](https://github.com/buildkite/interpolate). I'd
|
||||
like to thank the authors of that library for their work. I've forked it to make some changes that I
|
||||
needed for my own use cases, and to make it easier to maintain. I've also added some tests and
|
||||
documentation.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under MIT license, in `LICENSE`.
|
||||
53
vendor/github.com/mfridman/interpolate/env.go
generated
vendored
Normal file
53
vendor/github.com/mfridman/interpolate/env.go
generated
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package interpolate
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Env is an interface for getting environment variables by name and returning a boolean indicating
|
||||
// whether the variable was found.
|
||||
type Env interface {
|
||||
Get(key string) (string, bool)
|
||||
}
|
||||
|
||||
// NewSliceEnv creates an Env from a slice of environment variables in the form "key=value".
|
||||
//
|
||||
// This can be used with [os.Environ] to create an Env.
|
||||
func NewSliceEnv(env []string) Env {
|
||||
envMap := mapEnv{}
|
||||
for _, l := range env {
|
||||
parts := strings.SplitN(l, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
envMap[normalizeKeyName(parts[0])] = parts[1]
|
||||
}
|
||||
}
|
||||
return envMap
|
||||
}
|
||||
|
||||
// NewMapEnv creates an Env from a map of environment variables.
|
||||
func NewMapEnv(env map[string]string) Env {
|
||||
envMap := mapEnv{}
|
||||
for k, v := range env {
|
||||
envMap[normalizeKeyName(k)] = v
|
||||
}
|
||||
return envMap
|
||||
}
|
||||
|
||||
type mapEnv map[string]string
|
||||
|
||||
func (m mapEnv) Get(key string) (string, bool) {
|
||||
if m == nil {
|
||||
return "", false
|
||||
}
|
||||
val, ok := m[normalizeKeyName(key)]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Windows isn't case sensitive for env
|
||||
func normalizeKeyName(key string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return strings.ToUpper(key)
|
||||
}
|
||||
return key
|
||||
}
|
||||
213
vendor/github.com/mfridman/interpolate/interpolate.go
generated
vendored
Normal file
213
vendor/github.com/mfridman/interpolate/interpolate.go
generated
vendored
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
package interpolate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Interpolate takes a set of environment and interpolates it into the provided string using shell
|
||||
// script expansions
|
||||
func Interpolate(env Env, str string) (string, error) {
|
||||
if env == nil {
|
||||
env = NewSliceEnv(nil)
|
||||
}
|
||||
expr, err := NewParser(str).Parse()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return expr.Expand(env)
|
||||
}
|
||||
|
||||
// Indentifiers parses the identifiers from any expansions in the provided string
|
||||
func Identifiers(str string) ([]string, error) {
|
||||
expr, err := NewParser(str).Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return expr.Identifiers(), nil
|
||||
}
|
||||
|
||||
// An expansion is something that takes in ENV and returns a string or an error
|
||||
type Expansion interface {
|
||||
Expand(env Env) (string, error)
|
||||
Identifiers() []string
|
||||
}
|
||||
|
||||
// VariableExpansion represents either $VAR or ${VAR}, our simplest expansion
|
||||
type VariableExpansion struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
func (e VariableExpansion) Identifiers() []string {
|
||||
return []string{e.Identifier}
|
||||
}
|
||||
|
||||
func (e VariableExpansion) Expand(env Env) (string, error) {
|
||||
val, _ := env.Get(e.Identifier)
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// EmptyValueExpansion returns either the value of an env, or a default value if it's unset or null
|
||||
type EmptyValueExpansion struct {
|
||||
Identifier string
|
||||
Content Expression
|
||||
}
|
||||
|
||||
func (e EmptyValueExpansion) Identifiers() []string {
|
||||
return append([]string{e.Identifier}, e.Content.Identifiers()...)
|
||||
}
|
||||
|
||||
func (e EmptyValueExpansion) Expand(env Env) (string, error) {
|
||||
val, _ := env.Get(e.Identifier)
|
||||
if val == "" {
|
||||
return e.Content.Expand(env)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// UnsetValueExpansion returns either the value of an env, or a default value if it's unset
|
||||
type UnsetValueExpansion struct {
|
||||
Identifier string
|
||||
Content Expression
|
||||
}
|
||||
|
||||
func (e UnsetValueExpansion) Identifiers() []string {
|
||||
return []string{e.Identifier}
|
||||
}
|
||||
|
||||
func (e UnsetValueExpansion) Expand(env Env) (string, error) {
|
||||
val, ok := env.Get(e.Identifier)
|
||||
if !ok {
|
||||
return e.Content.Expand(env)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// SubstringExpansion returns a substring (or slice) of the env
|
||||
type SubstringExpansion struct {
|
||||
Identifier string
|
||||
Offset int
|
||||
Length int
|
||||
HasLength bool
|
||||
}
|
||||
|
||||
func (e SubstringExpansion) Identifiers() []string {
|
||||
return []string{e.Identifier}
|
||||
}
|
||||
|
||||
func (e SubstringExpansion) Expand(env Env) (string, error) {
|
||||
val, _ := env.Get(e.Identifier)
|
||||
|
||||
from := e.Offset
|
||||
|
||||
// Negative offsets = from end
|
||||
if from < 0 {
|
||||
from += len(val)
|
||||
}
|
||||
|
||||
// Still negative = too far from end? Truncate to start.
|
||||
if from < 0 {
|
||||
from = 0
|
||||
}
|
||||
|
||||
// Beyond end? Truncate to end.
|
||||
if from > len(val) {
|
||||
from = len(val)
|
||||
}
|
||||
|
||||
if !e.HasLength {
|
||||
return val[from:], nil
|
||||
}
|
||||
|
||||
to := e.Length
|
||||
|
||||
if to >= 0 {
|
||||
// Positive length = from offset
|
||||
to += from
|
||||
} else {
|
||||
// Negative length = from end
|
||||
to += len(val)
|
||||
|
||||
// Too far? Truncate to offset.
|
||||
if to < from {
|
||||
to = from
|
||||
}
|
||||
}
|
||||
|
||||
// Beyond end? Truncate to end.
|
||||
if to > len(val) {
|
||||
to = len(val)
|
||||
}
|
||||
|
||||
return val[from:to], nil
|
||||
}
|
||||
|
||||
// RequiredExpansion returns an env value, or an error if it is unset
|
||||
type RequiredExpansion struct {
|
||||
Identifier string
|
||||
Message Expression
|
||||
}
|
||||
|
||||
func (e RequiredExpansion) Identifiers() []string {
|
||||
return []string{e.Identifier}
|
||||
}
|
||||
|
||||
func (e RequiredExpansion) Expand(env Env) (string, error) {
|
||||
val, ok := env.Get(e.Identifier)
|
||||
if !ok {
|
||||
msg, err := e.Message.Expand(env)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if msg == "" {
|
||||
msg = "not set"
|
||||
}
|
||||
return "", fmt.Errorf("$%s: %s", e.Identifier, msg)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Expression is a collection of either Text or Expansions
|
||||
type Expression []ExpressionItem
|
||||
|
||||
func (e Expression) Identifiers() []string {
|
||||
identifiers := []string{}
|
||||
for _, item := range e {
|
||||
if item.Expansion != nil {
|
||||
identifiers = append(identifiers, item.Expansion.Identifiers()...)
|
||||
}
|
||||
}
|
||||
return identifiers
|
||||
}
|
||||
|
||||
func (e Expression) Expand(env Env) (string, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
for _, item := range e {
|
||||
if item.Expansion != nil {
|
||||
result, err := item.Expansion.Expand(env)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, _ = buf.WriteString(result)
|
||||
} else {
|
||||
_, _ = buf.WriteString(item.Text)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// ExpressionItem models either an Expansion or Text. Either/Or, never both.
|
||||
type ExpressionItem struct {
|
||||
Text string
|
||||
// -- or --
|
||||
Expansion Expansion
|
||||
}
|
||||
|
||||
func (i ExpressionItem) String() string {
|
||||
if i.Expansion != nil {
|
||||
return fmt.Sprintf("%#v", i.Expansion)
|
||||
}
|
||||
return fmt.Sprintf("%q", i.Text)
|
||||
}
|
||||
281
vendor/github.com/mfridman/interpolate/parser.go
generated
vendored
Normal file
281
vendor/github.com/mfridman/interpolate/parser.go
generated
vendored
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
package interpolate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// This is a recursive descent parser for our grammar. Because it can contain nested expressions
|
||||
// like ${LLAMAS:-${ROCK:-true}} we can't use regular expressions. The simplest possible alternative
|
||||
// is a recursive parser like this. It parses a chunk and then calls a function to parse that
|
||||
// further and so on and so forth. It results in a tree of objects that represent the things we've
|
||||
// parsed (an AST). This means that the logic for how expansions work lives in those objects, and
|
||||
// the logic for how we go from plain text to parsed objects lives here.
|
||||
//
|
||||
// To keep things simple, we do our "lexing" or "scanning" just as a few functions at the end of the
|
||||
// file rather than as a dedicated lexer that emits tokens. This matches the simplicity of the
|
||||
// format we are parsing relatively well
|
||||
//
|
||||
// Below is an EBNF grammar for the language. The parser was built by basically turning this into
|
||||
// functions and structs named the same reading the string bite by bite (peekRune and nextRune)
|
||||
|
||||
/*
|
||||
EscapedBackslash = "\\" EscapedDollar = ( "\$" | "$$") Identifier = letter { letters |
|
||||
digit | "_" } Expansion = "$" ( Identifier | Brace ) Brace = "{" Identifier [
|
||||
Identifier BraceOperation ] "}" Text = { EscapedBackslash | EscapedDollar | all characters except
|
||||
"$" } Expression = { Text | Expansion } EmptyValue = ":-" { Expression } UnsetValue =
|
||||
"-" { Expression } Substring = ":" number [ ":" number ] Required = "?" { Expression }
|
||||
Operation = EmptyValue | UnsetValue | Substring | Required
|
||||
*/
|
||||
|
||||
const (
|
||||
eof = -1
|
||||
)
|
||||
|
||||
// Parser takes a string and parses out a tree of structs that represent text and Expansions
|
||||
type Parser struct {
|
||||
input string // the string we are scanning
|
||||
pos int // the current position
|
||||
}
|
||||
|
||||
// NewParser returns a new instance of a Parser
|
||||
func NewParser(str string) *Parser {
|
||||
return &Parser{
|
||||
input: str,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse expansions out of the internal text and return them as a tree of Expressions
|
||||
func (p *Parser) Parse() (Expression, error) {
|
||||
return p.parseExpression()
|
||||
}
|
||||
|
||||
func (p *Parser) parseExpression(stop ...rune) (Expression, error) {
|
||||
var expr Expression
|
||||
var stopStr = string(stop)
|
||||
|
||||
for {
|
||||
c := p.peekRune()
|
||||
if c == eof || strings.ContainsRune(stopStr, c) {
|
||||
break
|
||||
}
|
||||
|
||||
// check for our escaped characters first, as we assume nothing subsequently is escaped
|
||||
if strings.HasPrefix(p.input[p.pos:], `\\`) {
|
||||
p.pos += 2
|
||||
expr = append(expr, ExpressionItem{Text: `\\`})
|
||||
continue
|
||||
} else if strings.HasPrefix(p.input[p.pos:], `\$`) || strings.HasPrefix(p.input[p.pos:], `$$`) {
|
||||
p.pos += 2
|
||||
expr = append(expr, ExpressionItem{Text: `$`})
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore bash shell expansions
|
||||
if strings.HasPrefix(p.input[p.pos:], `$(`) {
|
||||
p.pos += 2
|
||||
expr = append(expr, ExpressionItem{Text: `$(`})
|
||||
continue
|
||||
}
|
||||
|
||||
// If we run into a dollar sign and it's not the last char, it's an expansion
|
||||
if c == '$' && p.pos < (len(p.input)-1) {
|
||||
expansion, err := p.parseExpansion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expr = append(expr, ExpressionItem{Expansion: expansion})
|
||||
continue
|
||||
}
|
||||
|
||||
// nibble a character, otherwise if it's a \ or a $ we can loop
|
||||
c = p.nextRune()
|
||||
|
||||
// Scan as much as we can into text
|
||||
text := p.scanUntil(func(r rune) bool {
|
||||
return (r == '$' || r == '\\' || strings.ContainsRune(stopStr, r))
|
||||
})
|
||||
|
||||
expr = append(expr, ExpressionItem{Text: string(c) + text})
|
||||
}
|
||||
|
||||
return expr, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseExpansion() (Expansion, error) {
|
||||
if c := p.nextRune(); c != '$' {
|
||||
return nil, fmt.Errorf("Expected expansion to start with $, got %c", c)
|
||||
}
|
||||
|
||||
// if we have an open brace, this is a brace expansion
|
||||
if c := p.peekRune(); c == '{' {
|
||||
return p.parseBraceExpansion()
|
||||
}
|
||||
|
||||
identifier, err := p.scanIdentifier()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return VariableExpansion{Identifier: identifier}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseBraceExpansion() (Expansion, error) {
|
||||
if c := p.nextRune(); c != '{' {
|
||||
return nil, fmt.Errorf("Expected brace expansion to start with {, got %c", c)
|
||||
}
|
||||
|
||||
identifier, err := p.scanIdentifier()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c := p.peekRune(); c == '}' {
|
||||
_ = p.nextRune()
|
||||
return VariableExpansion{Identifier: identifier}, nil
|
||||
}
|
||||
|
||||
var operator string
|
||||
var exp Expansion
|
||||
|
||||
// Parse an operator, some trickery is needed to handle : vs :-
|
||||
if op1 := p.nextRune(); op1 == ':' {
|
||||
if op2 := p.peekRune(); op2 == '-' {
|
||||
_ = p.nextRune()
|
||||
operator = ":-"
|
||||
} else {
|
||||
operator = ":"
|
||||
}
|
||||
} else if op1 == '?' || op1 == '-' {
|
||||
operator = string(op1)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Expected an operator, got %c", op1)
|
||||
}
|
||||
|
||||
switch operator {
|
||||
case `:-`:
|
||||
exp, err = p.parseEmptyValueExpansion(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case `-`:
|
||||
exp, err = p.parseUnsetValueExpansion(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case `:`:
|
||||
exp, err = p.parseSubstringExpansion(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case `?`:
|
||||
exp, err = p.parseRequiredExpansion(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if c := p.nextRune(); c != '}' {
|
||||
return nil, fmt.Errorf("Expected brace expansion to end with }, got %c", c)
|
||||
}
|
||||
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseEmptyValueExpansion(identifier string) (Expansion, error) {
|
||||
// parse an expression (text and expansions) up until the end of the brace
|
||||
expr, err := p.parseExpression('}')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return EmptyValueExpansion{Identifier: identifier, Content: expr}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseUnsetValueExpansion(identifier string) (Expansion, error) {
|
||||
expr, err := p.parseExpression('}')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return UnsetValueExpansion{Identifier: identifier, Content: expr}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseSubstringExpansion(identifier string) (Expansion, error) {
|
||||
offset := p.scanUntil(func(r rune) bool {
|
||||
return r == ':' || r == '}'
|
||||
})
|
||||
|
||||
offsetInt, err := strconv.Atoi(strings.TrimSpace(offset))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse offset: %v", err)
|
||||
}
|
||||
|
||||
if c := p.peekRune(); c == '}' {
|
||||
return SubstringExpansion{Identifier: identifier, Offset: offsetInt}, nil
|
||||
}
|
||||
|
||||
_ = p.nextRune()
|
||||
length := p.scanUntil(func(r rune) bool {
|
||||
return r == '}'
|
||||
})
|
||||
|
||||
lengthInt, err := strconv.Atoi(strings.TrimSpace(length))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse length: %v", err)
|
||||
}
|
||||
|
||||
return SubstringExpansion{Identifier: identifier, Offset: offsetInt, Length: lengthInt, HasLength: true}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseRequiredExpansion(identifier string) (Expansion, error) {
|
||||
expr, err := p.parseExpression('}')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return RequiredExpansion{Identifier: identifier, Message: expr}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) scanUntil(f func(rune) bool) string {
|
||||
start := p.pos
|
||||
for int(p.pos) < len(p.input) {
|
||||
c, size := utf8.DecodeRuneInString(p.input[p.pos:])
|
||||
if c == utf8.RuneError || f(c) {
|
||||
break
|
||||
}
|
||||
p.pos += size
|
||||
}
|
||||
return p.input[start:p.pos]
|
||||
}
|
||||
|
||||
func (p *Parser) scanIdentifier() (string, error) {
|
||||
if c := p.peekRune(); !unicode.IsLetter(c) {
|
||||
return "", fmt.Errorf("Expected identifier to start with a letter, got %c", c)
|
||||
}
|
||||
var notIdentifierChar = func(r rune) bool {
|
||||
return (!unicode.IsLetter(r) && !unicode.IsNumber(r) && r != '_')
|
||||
}
|
||||
return p.scanUntil(notIdentifierChar), nil
|
||||
}
|
||||
|
||||
func (p *Parser) nextRune() rune {
|
||||
if int(p.pos) >= len(p.input) {
|
||||
return eof
|
||||
}
|
||||
c, size := utf8.DecodeRuneInString(p.input[p.pos:])
|
||||
p.pos += size
|
||||
return c
|
||||
}
|
||||
|
||||
func (p *Parser) peekRune() rune {
|
||||
if int(p.pos) >= len(p.input) {
|
||||
return eof
|
||||
}
|
||||
c, _ := utf8.DecodeRuneInString(p.input[p.pos:])
|
||||
return c
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue