Skip to content

Commit f32450f

Browse files
authored
feat(spaced): Add spaced utility (#1)
1 parent f5050b8 commit f32450f

15 files changed

+541
-0
lines changed

.drone.yml

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
kind: pipeline
2+
name: default
3+
4+
platform:
5+
os: linux
6+
arch: amd64
7+
8+
steps:
9+
- name: lint + test
10+
image: golang:1.11.2
11+
commands:
12+
- go get -u golang.org/x/lint/golint
13+
- ./scripts/lint-test.sh
14+
when:
15+
event:
16+
exclude:
17+
- pull_request
18+
- tag
19+
20+
- name: build + package
21+
image: golang:1.11.2
22+
commands:
23+
- apt-get update && apt-get install --assume-yes zip
24+
- ./scripts/build-package.sh
25+
environment:
26+
OUTDIR: /tmp/dist
27+
volumes:
28+
- name: dist
29+
path: /tmp/dist
30+
when:
31+
event:
32+
exclude:
33+
- pull_request
34+
- tag
35+
36+
- name: publish
37+
image: node:10.14.0
38+
commands:
39+
- npx semantic-release
40+
environment:
41+
GITHUB_TOKEN:
42+
from_secret: github/public_repo
43+
# https://github.com/npm/uid-number/issues/7
44+
NPM_CONFIG_UNSAFE_PERM: true
45+
volumes:
46+
- name: dist
47+
path: /tmp/dist
48+
when:
49+
branch:
50+
- master
51+
event:
52+
- push
53+
54+
volumes:
55+
- name: dist
56+
temp: {}

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
.temp/
12
.vscode/
3+
dist/
24
vendor/
35

46
.DS_Store

.releaserc.yml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
repositoryUrl: git@github.com:72636c/hyperspaced
2+
branch: master
3+
4+
ci: true
5+
debug: true
6+
7+
plugins:
8+
- '@semantic-release/commit-analyzer'
9+
- '@semantic-release/release-notes-generator'
10+
- - '@semantic-release/github'
11+
- assets: '/tmp/dist/**'
12+
failComment: false
13+
failTitle: false
14+
labels: false
15+
releasedLabels: false
16+
successComment: false

README.md

+53
Original file line numberDiff line numberDiff line change
@@ -1 +1,54 @@
11
# H Y P E R S P A C E D
2+
3+
[![Build Status](https://cloud.drone.io/api/badges/72636c/hyperspaced/status.svg)](https://cloud.drone.io/72636c/hyperspaced)
4+
5+
Command line utilities to improve the aesthetics of your favourite phrases with
6+
automatic space insertion (ASI).
7+
8+
## Usage
9+
10+
### CLI
11+
12+
Prerequisites:
13+
14+
- Go 1.11+
15+
16+
```shell
17+
go install ./cmd/spaced
18+
```
19+
20+
```shell
21+
echo 'gofmt urself' | spaced
22+
```
23+
24+
### Go
25+
26+
```go
27+
import (
28+
"github.com/72636c/hyperspaced"
29+
)
30+
31+
hyperspaced.Spaced("gofmt urself")
32+
```
33+
34+
## Meta
35+
36+
### CI/CD pipeline
37+
38+
Builds and releases are automated with [Drone](https://drone.io/), which is
39+
impressively _"powered by blazing fast bare-metal servers"_, and more
40+
importantly _"written in Go"_.
41+
42+
### Local scripts
43+
44+
```shell
45+
./scripts/build-package.sh
46+
```
47+
48+
```shell
49+
./scripts/lint-test.sh
50+
```
51+
52+
### Mouseprint
53+
54+
_This is a shameless rip-off of <https://www.npmjs.com/package/letter-spacing>._

cmd/spaced/main.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/72636c/hyperspaced/internal/text"
8+
"github.com/72636c/hyperspaced/internal/text/transform"
9+
)
10+
11+
func main() {
12+
err := text.LineFilter(transform.Spaced)
13+
if err != nil {
14+
fmt.Fprintln(os.Stderr, "error: ", err)
15+
os.Exit(1)
16+
}
17+
}

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/72636c/hyperspaced
2+
3+
require golang.org/x/text v0.3.0

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
2+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

hyperspaced.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package hyperspaced
2+
3+
import (
4+
"github.com/72636c/hyperspaced/internal/text/transform"
5+
)
6+
7+
// Spaced inserts a space between each character in a string.
8+
func Spaced(str string) string {
9+
return transform.Spaced(str)
10+
}
11+
12+
// SpacedN inserts n spaces between each character in a string.
13+
func SpacedN(str string, n int) string {
14+
return transform.SpacedN(str, n)
15+
}

internal/text/char.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package text
2+
3+
import (
4+
"unicode"
5+
"unicode/utf8"
6+
7+
"golang.org/x/text/unicode/norm"
8+
)
9+
10+
// Char represents a character. Whatever that means.
11+
type Char []rune
12+
13+
func (char Char) String() (str string) {
14+
for _, r := range char {
15+
str += string(r)
16+
}
17+
18+
return
19+
}
20+
21+
// SplitString splits a string into a slice of characters.
22+
//
23+
// It's obviously not perfect; don't @ me.
24+
func SplitString(str string) []Char {
25+
var iter norm.Iter
26+
iter.InitString(norm.NFC, str)
27+
28+
chars := make([]Char, 0)
29+
runes := make([]rune, 0)
30+
31+
for !iter.Done() {
32+
r, _ := utf8.DecodeRune(iter.Next())
33+
34+
if areSeparate(runes, r) {
35+
chars = append(chars, Char(runes))
36+
runes = []rune{r}
37+
} else {
38+
runes = append(runes, r)
39+
}
40+
}
41+
42+
if len(runes) == 0 {
43+
return chars
44+
}
45+
46+
return append(chars, Char(runes))
47+
}
48+
49+
func areSeparate(runes []rune, next rune) bool {
50+
if len(runes) == 0 {
51+
return false
52+
}
53+
54+
previous := runes[len(runes)-1]
55+
56+
return !isNull(previous) &&
57+
!isJoinControl(previous) &&
58+
!isJoinControl(next) &&
59+
!isModifierSymbol(next) &&
60+
!isVariationSelector(next)
61+
}
62+
63+
// https://www.fileformat.info/info/unicode/char/0000/index.htm
64+
func isNull(r rune) bool {
65+
return r == '\u0000'
66+
}
67+
68+
// http://unicode.org/reports/tr44/#Join_Control
69+
func isJoinControl(r rune) bool {
70+
return unicode.In(r, unicode.Join_Control)
71+
}
72+
73+
// http://www.fileformat.info/info/unicode/category/Sk/list.htm
74+
func isModifierSymbol(r rune) bool {
75+
return unicode.In(r, unicode.Sk)
76+
}
77+
78+
// http://www.unicode.org/charts/PDF/UFE00.pdf
79+
func isVariationSelector(r rune) bool {
80+
return unicode.In(r, unicode.Variation_Selector)
81+
}

internal/text/char_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package text_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/72636c/hyperspaced/internal/text"
8+
)
9+
10+
func Test_SplitString(t *testing.T) {
11+
testCases := []struct {
12+
description string
13+
input string
14+
expected []text.Char
15+
}{
16+
{
17+
description: "Empty String",
18+
input: "",
19+
expected: []text.Char{},
20+
},
21+
{
22+
description: "One Letter",
23+
input: "f",
24+
expected: []text.Char{text.Char{'f'}},
25+
},
26+
{
27+
description: "Two Letters",
28+
input: "hi",
29+
expected: []text.Char{text.Char{'h'}, text.Char{'i'}},
30+
},
31+
{
32+
description: "Accent Normalisation",
33+
input: "e\u0301",
34+
expected: []text.Char{text.Char{'é'}},
35+
},
36+
{
37+
description: "Separate Emoji",
38+
input: "\U0001f30f\u2696",
39+
expected: []text.Char{text.Char{'🌏'}, text.Char{'⚖'}},
40+
},
41+
{
42+
// https://emojipedia.org/family-man-light-skin-tone-woman-light-skin-tone-girl-light-skin-tone-baby-light-skin-tone/
43+
description: "Sequenced Emoji",
44+
input: "\U0001f468\U0001f3fb\u200d\U0001f469\U0001f3fb\u200d\U0001f467\U0001f3fb\u200d\U0001f476\U0001f3fb",
45+
expected: []text.Char{text.Char{'👨', '🏻', '\u200d', '👩', '🏻', '\u200d', '👧', '🏻', '\u200d', '👶', '🏻'}},
46+
},
47+
{
48+
// https://emojipedia.org/female-sleuth/
49+
description: "Female Variant Selector",
50+
input: "\U0001f575\ufe0f\u200d\u2640\ufe0f",
51+
expected: []text.Char{text.Char{'🕵', '\ufe0f', '\u200d', '♀', '\ufe0f'}},
52+
},
53+
{
54+
// https://emojipedia.org/female-technologist/
55+
description: "Join Control",
56+
input: "\U0001f469\u200d\U0001f4bb",
57+
expected: []text.Char{text.Char{'👩', '\u200d', '💻'}},
58+
},
59+
{
60+
// https://emojipedia.org/man-type-6/
61+
description: "Skin Tone Modifier",
62+
input: "\U0001f468\U0001f3ff",
63+
expected: []text.Char{text.Char{'👨', '🏿'}},
64+
},
65+
}
66+
67+
for _, testCase := range testCases {
68+
t.Run(testCase.description, func(t *testing.T) {
69+
actual := text.SplitString(testCase.input)
70+
if !reflect.DeepEqual(actual, testCase.expected) {
71+
t.Errorf("expected %+v, received %+v", testCase.expected, actual)
72+
}
73+
})
74+
}
75+
}

internal/text/lineFilter.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package text
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
)
8+
9+
// TransformLine is a function that transforms a line of text.
10+
type TransformLine func(string) string
11+
12+
// LineFilter transforms lines of text as they pass from stdin to stdout.
13+
func LineFilter(transform TransformLine) error {
14+
scanner := bufio.NewScanner(os.Stdin)
15+
16+
for scanner.Scan() {
17+
output := transform(scanner.Text())
18+
19+
_, err := fmt.Println(output)
20+
if err != nil {
21+
return err
22+
}
23+
}
24+
25+
return scanner.Err()
26+
}

0 commit comments

Comments
 (0)