🐀

gosimple/slug slugに適した変換をしてくれるライブラリを紹介

2022/01/20に公開

概要

RealWorld で Go の勉強をしているときにgosimplel/slugというライブラリを発見しました。
便利そうだったのですが、ググっても紹介記事がなかったので紹介することにしました。

https://github.com/gosimple/slug

使用例

タイトルに書いた通り、文字列を slug に適した変換をしてくれます。
以下はリポジトリの go.doc の例です。

https://pkg.go.dev/github.com/gosimple/slug@v1.12.0

package main

import (
	"fmt"
	"github.com/gosimple/slug"
)

func main() {
	text := slug.Make("Hellö Wörld хелло ворлд")
	fmt.Println(text) // Will print: "hello-world-khello-vorld"

	someText := slug.Make("影師")
	fmt.Println(someText) // Will print: "ying-shi"

	enText := slug.MakeLang("This & that", "en")
	fmt.Println(enText) // Will print: "this-and-that"

	deText := slug.MakeLang("Diese & Dass", "de")
	fmt.Println(deText) // Will print: "diese-und-dass"

	slug.Lowercase = false // Keep uppercase characters
	deUppercaseText := slug.MakeLang("Diese & Dass", "de")
	fmt.Println(deUppercaseText) // Will print: "Diese-und-Dass"

	slug.CustomSub = map[string]string{
		"water": "sand",
	}
	textSub := slug.Make("water is hot")
	fmt.Println(textSub) // Will print: "sand-is-hot"
}

関数

slug.Make()では引数を、英語に変換してくれます。
また、url に使用できない文字はそれぞれ変換してくれます(スペースはハイフンに、&andに変換、etc...)。
日本語にも対応しています(たとえば、タイトルは taitoru)。

	text := slug.Make("Hellö Wörld хелло ворлд")
	fmt.Println(text) // Will print: "hello-world-khello-vorld"

	someText := slug.Make("影師")
	fmt.Println(someText) // Will print: "ying-shi"

slug.MakeLang()では、第二引数に自然言語を指定します。
ただ、指定できる自然言語は一部のアルファベットで表記できる言語のみです(後述)。

	enText := slug.MakeLang("This & that", "en")
	fmt.Println(enText) // Will print: "this-and-that"

	deText := slug.MakeLang("Diese & Dass", "de")
	fmt.Println(deText) // Will print: "diese-und-dass"

他には slug 判定するIsSlug()が実装されています。

オプション

変換のオプションを設定できます。
slug.Lowercase = falseで小文字変換を無効にしたり、slug.CustomSubで独自の変換を設定できたりします。

	slug.Lowercase = false // Keep uppercase characters
	deUppercaseText := slug.MakeLang("Diese & Dass", "de")
	fmt.Println(deUppercaseText) // Will print: "Diese-und-Dass"

	slug.CustomSub = map[string]string{
		"water": "sand",
	}
	textSub := slug.Make("water is hot")
	fmt.Println(textSub) // Will print: "sand-is-hot"

紹介された例以外には、以下の項目が設定できます。

var (
	// CustomSub stores custom substitution map
	CustomSub map[string]string
	// CustomRuneSub stores custom rune substitution map
	CustomRuneSub map[rune]string

	// MaxLength stores maximum slug length.
	// It's smart so it will cat slug after full word.
	// By default slugs aren't shortened.
	// If MaxLength is smaller than length of the first word, then returned
	// slug will contain only substring from the first word truncated
	// after MaxLength.
	MaxLength int

	// Lowercase defines if the resulting slug is transformed to lowercase.
	// Default is true.
	Lowercase = true
)

実装の確認

実際に slug.go を確認してみました。

https://github.com/gosimple/slug/blob/master/slug.go

Make()は、内部的にMakeLang()を第二引数"en"で呼び出していました。

// Make returns slug generated from provided string. Will use "en" as language
// substitution.
func Make(s string) (slug string) {
	return MakeLang(s, "en")
}

MakeLang()は、スペースの削除(TrimSpace)、ユーザー設定(SubstituteRuneSubstitute)の変換後、アルファベット変換します。
17 種類の言語が設定されており、どれにも該当しなかった場合自動的に英語変換になります(jaとかchとかは存在しないのでenになる)。

// MakeLang returns slug generated from provided string and will use provided
// language for chars substitution.
func MakeLang(s string, lang string) (slug string) {
	slug = strings.TrimSpace(s)

	// Custom substitutions
	// Always substitute runes first
	slug = SubstituteRune(slug, CustomRuneSub)
	slug = Substitute(slug, CustomSub)

	// Process string with selected substitution language.
	// Catch ISO 3166-1, ISO 639-1:2002 and ISO 639-3:2007.
	switch strings.ToLower(lang) {
	case "cs", "ces":
		slug = SubstituteRune(slug, csSub)
	case "de", "deu":
		slug = SubstituteRune(slug, deSub)
	case "en", "eng":
		slug = SubstituteRune(slug, enSub)
	case "es", "spa":
		slug = SubstituteRune(slug, esSub)
	case "fi", "fin":
		slug = SubstituteRune(slug, fiSub)
	case "fr", "fra":
		slug = SubstituteRune(slug, frSub)
	case "gr", "el", "ell":
		slug = SubstituteRune(slug, grSub)
	case "hu", "hun":
		slug = SubstituteRune(slug, huSub)
	case "id", "idn", "ind":
		slug = SubstituteRune(slug, idSub)
	case "kz", "kk", "kaz":
		slug = SubstituteRune(slug, kkSub)
	case "nb", "nob":
		slug = SubstituteRune(slug, nbSub)
	case "nl", "nld":
		slug = SubstituteRune(slug, nlSub)
	case "nn", "nno":
		slug = SubstituteRune(slug, nnSub)
	case "pl", "pol":
		slug = SubstituteRune(slug, plSub)
	case "sl", "slv":
		slug = SubstituteRune(slug, slSub)
	case "sv", "swe":
		slug = SubstituteRune(slug, svSub)
	case "tr", "tur":
		slug = SubstituteRune(slug, trSub)
	default: // fallback to "en" if lang not found
		slug = SubstituteRune(slug, enSub)
	}

	// Process all non ASCII symbols
	slug = unidecode.Unidecode(slug)

	if Lowercase {
		slug = strings.ToLower(slug)
	}

	// Process all remaining symbols
	slug = regexpNonAuthorizedChars.ReplaceAllString(slug, "-")
	slug = regexpMultipleDashes.ReplaceAllString(slug, "-")
	slug = strings.Trim(slug, "-_")

	if MaxLength > 0 {
		slug = smartTruncate(slug)
	}

	return slug
}

他にも、slug 判定するIsSlugは以下のように実装されていることがわかりました。

// IsSlug returns True if provided text does not contain white characters,
// punctuation, all letters are lower case and only from ASCII range.
// It could contain `-` and `_` but not at the beginning or end of the text.
// It should be in range of the MaxLength var if specified.
// All output from slug.Make(text) should pass this test.
func IsSlug(text string) bool {
	if text == "" ||
		(MaxLength > 0 && len(text) > MaxLength) ||
		text[0] == '-' || text[0] == '_' ||
		text[len(text)-1] == '-' || text[len(text)-1] == '_' {
		return false
	}
	for _, c := range text {
		if (c < 'a' || c > 'z') && c != '-' && c != '_' && (c < '0' || c > '9') {
			return false
		}
	}
	return true
}

説明は書略しますが、言語ごとの変換パターンはlanguages_substitution.goで実装されています。

https://github.com/gosimple/slug/blob/v1.12.0/languages_substitution.go

まとめ

文字列を slug に適した変換をしてくれるgosimple/slugについて紹介しました。
slug の自動生成だけでなく脆弱性も排除されそうですので、使ってみてはどうでしょうか。

Discussion