replace zxq.co/ripple/hanayo
This commit is contained in:
229
modules/bbcode/bbcode.go
Normal file
229
modules/bbcode/bbcode.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// Package bbcode implements BBCode compiling for Hanayo.
|
||||
package bbcode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/frustra/bbcode"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
var bbcodeCompiler = func() bbcode.Compiler {
|
||||
compiler := bbcode.NewCompiler(true, true)
|
||||
compiler.SetTag("list", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
out := bbcode.NewHTMLTag("")
|
||||
out.Name = "ul"
|
||||
style := node.GetOpeningTag().Value
|
||||
switch style {
|
||||
case "a":
|
||||
out.Attrs["style"] = "list-style-type: lower-alpha;"
|
||||
case "A":
|
||||
out.Attrs["style"] = "list-style-type: upper-alpha;"
|
||||
case "i":
|
||||
out.Attrs["style"] = "list-style-type: lower-roman;"
|
||||
case "I":
|
||||
out.Attrs["style"] = "list-style-type: upper-roman;"
|
||||
case "1":
|
||||
out.Attrs["style"] = "list-style-type: decimal;"
|
||||
default:
|
||||
out.Attrs["style"] = "list-style-type: disc;"
|
||||
}
|
||||
|
||||
if len(node.Children) == 0 {
|
||||
out.AppendChild(bbcode.NewHTMLTag(""))
|
||||
} else {
|
||||
node.Info = []*bbcode.HTMLTag{out, out}
|
||||
tags := node.Info.([]*bbcode.HTMLTag)
|
||||
for _, child := range node.Children {
|
||||
curr := tags[1]
|
||||
curr.AppendChild(node.Compiler.CompileTree(child))
|
||||
}
|
||||
if len(tags[1].Children) > 0 {
|
||||
last := tags[1].Children[len(tags[1].Children)-1]
|
||||
if len(last.Children) > 0 && last.Children[len(last.Children)-1].Name == "br" {
|
||||
last.Children[len(last.Children)-1] = bbcode.NewHTMLTag("")
|
||||
}
|
||||
} else {
|
||||
tags[1].AppendChild(bbcode.NewHTMLTag(""))
|
||||
}
|
||||
}
|
||||
return out, false
|
||||
})
|
||||
|
||||
compiler.SetTag("*", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
parent := node.Parent
|
||||
for parent != nil {
|
||||
if parent.ID == bbcode.OPENING_TAG && parent.GetOpeningTag().Name == "list" {
|
||||
out := bbcode.NewHTMLTag("")
|
||||
out.Name = "li"
|
||||
tags := parent.Info.([]*bbcode.HTMLTag)
|
||||
if len(tags[1].Children) > 0 {
|
||||
last := tags[1].Children[len(tags[1].Children)-1]
|
||||
if len(last.Children) > 0 && last.Children[len(last.Children)-1].Name == "br" {
|
||||
last.Children[len(last.Children)-1] = bbcode.NewHTMLTag("")
|
||||
}
|
||||
} else {
|
||||
tags[1].AppendChild(bbcode.NewHTMLTag(""))
|
||||
}
|
||||
tags[1] = out
|
||||
tags[0].AppendChild(out)
|
||||
|
||||
if len(parent.Children) == 0 {
|
||||
out.AppendChild(bbcode.NewHTMLTag(""))
|
||||
} else {
|
||||
for _, child := range node.Children {
|
||||
curr := tags[1]
|
||||
curr.AppendChild(node.Compiler.CompileTree(child))
|
||||
}
|
||||
}
|
||||
if node.ClosingTag != nil {
|
||||
tag := bbcode.NewHTMLTag(node.ClosingTag.Raw)
|
||||
bbcode.InsertNewlines(tag)
|
||||
out.AppendChild(tag)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
parent = parent.Parent
|
||||
}
|
||||
return bbcode.DefaultTagCompiler(node)
|
||||
})
|
||||
|
||||
compiler.SetTag("youtube", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
var youtubeID string
|
||||
|
||||
content := bbcode.CompileText(node)
|
||||
youtubeLink, err := url.Parse(content)
|
||||
if err != nil {
|
||||
youtubeID = content
|
||||
} else {
|
||||
youtubeID = youtubeLink.Query().Get("v")
|
||||
if youtubeID == "" {
|
||||
youtubeID = content
|
||||
}
|
||||
}
|
||||
|
||||
tag := bbcode.NewHTMLTag("")
|
||||
tag.Name = "iframe"
|
||||
tag.Attrs = map[string]string{
|
||||
"style": "width: 100%; max-height: 100%;",
|
||||
"src": "https://www.youtube.com/embed/" + youtubeID,
|
||||
"frameborder": "0",
|
||||
"allowfullscreen": "",
|
||||
}
|
||||
tag.AppendChild(nil)
|
||||
|
||||
container := bbcode.NewHTMLTag("")
|
||||
container.Name = "div"
|
||||
container.Attrs["class"] = "youtube video container"
|
||||
container.AppendChild(tag)
|
||||
|
||||
return container, false
|
||||
})
|
||||
|
||||
compiler.SetTag("left", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
out := bbcode.NewHTMLTag("")
|
||||
out.Name = "div"
|
||||
out.Attrs["style"] = "text-align: left;"
|
||||
return out, true
|
||||
})
|
||||
compiler.SetTag("right", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
out := bbcode.NewHTMLTag("")
|
||||
out.Name = "div"
|
||||
out.Attrs["style"] = "text-align: right;"
|
||||
return out, true
|
||||
})
|
||||
|
||||
compiler.SetTag("container", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
args := node.GetOpeningTag().Args
|
||||
out := bbcode.NewHTMLTag("")
|
||||
out.Name = "div"
|
||||
out.Attrs["style"] = ""
|
||||
out.Attrs["class"] = ""
|
||||
if _, err := strconv.Atoi(args["width"]); err == nil {
|
||||
out.Attrs["style"] += "width: " + args["width"] + "px;"
|
||||
}
|
||||
if args["compact"] != "" {
|
||||
out.Attrs["class"] += "compact-container "
|
||||
}
|
||||
if args["center"] != "" {
|
||||
out.Attrs["style"] += "margin: 0 auto;"
|
||||
}
|
||||
return out, true
|
||||
})
|
||||
|
||||
compiler.SetTag("hr", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
out := bbcode.NewHTMLTag("")
|
||||
out.Name = "div"
|
||||
out.Attrs["class"] = "ui divider"
|
||||
out.AppendChild(nil)
|
||||
return out, false
|
||||
})
|
||||
|
||||
compiler.SetTag("email", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
out := bbcode.NewHTMLTag("")
|
||||
out.Name = "a"
|
||||
val := node.GetOpeningTag().Value
|
||||
if val == "" {
|
||||
out.Attrs["href"] = "mailto:" + bbcode.CompileText(node)
|
||||
out.AppendChild(bbcode.NewHTMLTag(bbcode.CompileText(node)))
|
||||
return out, false
|
||||
}
|
||||
out.Attrs["href"] = "mailto:" + val
|
||||
return out, true
|
||||
})
|
||||
|
||||
compiler.SetTag("size", func(node *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||
out := bbcode.NewHTMLTag("")
|
||||
out.Name = "span"
|
||||
if size, err := strconv.Atoi(node.GetOpeningTag().Value); err == nil && size > 0 {
|
||||
if size > 15 {
|
||||
size = 15
|
||||
}
|
||||
out.Attrs["style"] = fmt.Sprintf("font-size: %dpt; line-height: %[1]dpt;", size*6)
|
||||
}
|
||||
return out, true
|
||||
})
|
||||
|
||||
return compiler
|
||||
}()
|
||||
|
||||
var emojis = []string{
|
||||
"peppy",
|
||||
"barney",
|
||||
"akerino",
|
||||
"foka",
|
||||
"kappy",
|
||||
"creepypeppy",
|
||||
"peppyfiero",
|
||||
"djpeppy",
|
||||
"kappa",
|
||||
}
|
||||
var emojiReplacer = func() *strings.Replacer {
|
||||
var list []string
|
||||
for _, e := range emojis {
|
||||
list = append(list, ":"+e+":", "[img=/static/emotes/"+e+".png]:"+e+":[/img]")
|
||||
}
|
||||
return strings.NewReplacer(list...)
|
||||
}()
|
||||
|
||||
// Compile takes some BBCode and converts it into safe HTML output.
|
||||
func Compile(s string) string {
|
||||
s = emojiReplacer.Replace(s)
|
||||
s = strings.TrimSpace(s)
|
||||
return mondaySanitise(bbcodeCompiler.Compile(s))
|
||||
}
|
||||
|
||||
var policy = func() *bluemonday.Policy {
|
||||
p := bluemonday.UGCPolicy()
|
||||
p.AllowAttrs("style", "class").Globally()
|
||||
p.AllowElements("iframe")
|
||||
p.AllowAttrs("style", "src", "frameborder", "allowfullscreen").OnElements("iframe")
|
||||
return p
|
||||
}()
|
||||
|
||||
func mondaySanitise(source string) string {
|
||||
return policy.Sanitize(source)
|
||||
}
|
177
modules/btcaddress/btcaddress.go
Normal file
177
modules/btcaddress/btcaddress.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Package btcaddress makes sure the Bitcoin address for the Ripple donations
|
||||
// account on Keybase is up-to-date.
|
||||
// This shows a default key if the system is not set up, or otherwise refreshes
|
||||
// the address every 30 minutes (using redis to hold the cache).
|
||||
// The wallet with the name "Ripple" will be used for getting the address.
|
||||
package btcaddress
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"strings"
|
||||
|
||||
"gopkg.in/redis.v5"
|
||||
)
|
||||
|
||||
// DefaultAddress is the address used if the package is not set up to refresh
|
||||
// from coinbase.
|
||||
const DefaultAddress = "1CKGzZqrVwKoXwHEWpTobWqqtkYuqoFNro"
|
||||
|
||||
// AccountName is the name the account on coinbase must have to be considered as
|
||||
// the actual account.
|
||||
const AccountName = "Ripple"
|
||||
|
||||
// Configuration variables.
|
||||
var (
|
||||
Redis *redis.Client
|
||||
APIKey string
|
||||
APISecret string
|
||||
)
|
||||
|
||||
// Get retrieves the Bitcoin address, using the following methods:
|
||||
//
|
||||
// First the address is requested from Redis, and if the key is available from
|
||||
// redis then that is used.
|
||||
//
|
||||
// After that, the key is requested from coinbase, and the account ID is found
|
||||
// if not already saved in Redis.
|
||||
func Get() string {
|
||||
v := Redis.Get("hanayo:btcaddress").Val()
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
if APIKey == "" || APISecret == "" {
|
||||
return DefaultAddress
|
||||
}
|
||||
|
||||
a, err := getFromCoinbase()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return DefaultAddress
|
||||
}
|
||||
|
||||
Redis.Set("hanayo:btcaddress", a, time.Minute*30)
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
type account struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type accountsResp struct {
|
||||
Data []account `json:"data"`
|
||||
}
|
||||
|
||||
type address struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type createAddressResp struct {
|
||||
Data address `json:"data"`
|
||||
}
|
||||
|
||||
type errors struct {
|
||||
Errors []struct {
|
||||
ID string `json:"id"`
|
||||
Message string `json:"message"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
|
||||
const coinbaseAPIDate = "2017-01-23"
|
||||
const coinbaseAPIBase = "https://api.coinbase.com/v2/"
|
||||
|
||||
var accountID string
|
||||
|
||||
func getFromCoinbase() (string, error) {
|
||||
if accountID == "" {
|
||||
var accs accountsResp
|
||||
err := req("accounts", nil, &accs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, el := range accs.Data {
|
||||
if el.Name == AccountName {
|
||||
accountID = el.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var addrResp createAddressResp
|
||||
err := req("accounts/"+accountID+"/addresses", struct{}{}, &addrResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return addrResp.Data.Address, nil
|
||||
}
|
||||
|
||||
func req(endpoint string, data interface{}, out interface{}) error {
|
||||
var dataJSON string
|
||||
method := "GET"
|
||||
if data != nil {
|
||||
method = "POST"
|
||||
// sort of a dirty hack, but oh well
|
||||
djr, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dataJSON = string(djr)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, coinbaseAPIBase+endpoint, strings.NewReader(dataJSON))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
// Not usind .Header.Add because we want to make sure the names are kept
|
||||
// uppercase.
|
||||
ts := time.Now().Unix()
|
||||
req.Header["CB-VERSION"] = []string{coinbaseAPIDate}
|
||||
req.Header["CB-ACCESS-KEY"] = []string{APIKey}
|
||||
req.Header["CB-ACCESS-TIMESTAMP"] = []string{fmt.Sprintf("%d", ts)}
|
||||
|
||||
sig := fmt.Sprintf(
|
||||
"%d%s/v2/%s%s",
|
||||
ts,
|
||||
method,
|
||||
endpoint,
|
||||
string(dataJSON),
|
||||
)
|
||||
|
||||
hm := hmac.New(
|
||||
sha256.New,
|
||||
[]byte(APISecret),
|
||||
)
|
||||
hm.Write([]byte(sig))
|
||||
|
||||
req.Header["CB-ACCESS-SIGN"] = []string{
|
||||
fmt.Sprintf("%x", hm.Sum(nil)),
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 && resp.StatusCode != 201 {
|
||||
var errs errors
|
||||
json.NewDecoder(resp.Body).Decode(&errs)
|
||||
return fmt.Errorf("btcaddress: response status code is %d - %v", resp.StatusCode, errs)
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&out)
|
||||
return err
|
||||
}
|
54
modules/btcconversions/btcconversions.go
Normal file
54
modules/btcconversions/btcconversions.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package btcconversions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var rates = struct {
|
||||
EUR float64
|
||||
USD float64
|
||||
}{
|
||||
// 15m values fetched from blockchain ticker on 2017-02-25
|
||||
1118.33,
|
||||
1180.4,
|
||||
}
|
||||
|
||||
// GetRates returns the bitcoin rates as JSON.
|
||||
func GetRates(c *gin.Context) {
|
||||
c.JSON(200, rates)
|
||||
}
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
for {
|
||||
updateRates()
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type blockchainCurrency struct {
|
||||
// The X is necessary a) to make this exported b) because an identifier
|
||||
// can't start with a number.
|
||||
X15m float64 `json:"15m"`
|
||||
}
|
||||
|
||||
func updateRates() {
|
||||
resp, err := http.Get("https://blockchain.info/ticker")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
m := make(map[string]blockchainCurrency)
|
||||
json.NewDecoder(resp.Body).Decode(&m)
|
||||
|
||||
rates.EUR = m["EUR"].X15m
|
||||
rates.USD = m["USD"].X15m
|
||||
}
|
134
modules/doc/documentation.go
Normal file
134
modules/doc/documentation.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package doc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const referenceLanguage = "en"
|
||||
|
||||
var docFiles []Document
|
||||
|
||||
// File represents the single documentation file of a determined language.
|
||||
type File struct {
|
||||
IsUpdated bool
|
||||
Title string
|
||||
referencesFile string
|
||||
}
|
||||
|
||||
// Data retrieves data from file's actual file on disk.
|
||||
func (f File) Data() (string, error) {
|
||||
data, err := ioutil.ReadFile(f.referencesFile)
|
||||
updateIPs()
|
||||
res := strings.NewReplacer(
|
||||
"{ipmain}", ipMain,
|
||||
"{ipmirror}", ipMirror,
|
||||
).Replace(string(data))
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Document represents a documentation file, providing its old ID, its slug,
|
||||
// and all its variations in the various languages.
|
||||
type Document struct {
|
||||
Slug string
|
||||
OldID int
|
||||
Languages map[string]File
|
||||
}
|
||||
|
||||
// File retrieves a Document's File based on the passed language, and returns
|
||||
// the values for the referenceLanguage (en) if in the passed language they are
|
||||
// not available
|
||||
func (d Document) File(lang string) File {
|
||||
if vals, ok := d.Languages[lang]; ok {
|
||||
return vals
|
||||
}
|
||||
return d.Languages[referenceLanguage]
|
||||
}
|
||||
|
||||
// LanguageDoc has the only purpose to be returned by GetDocs.
|
||||
type LanguageDoc struct {
|
||||
Title string
|
||||
Slug string
|
||||
}
|
||||
|
||||
// GetDocs retrieves a list of documents in a certain language, with titles and
|
||||
// slugs.
|
||||
func GetDocs(lang string) []LanguageDoc {
|
||||
var docs []LanguageDoc
|
||||
|
||||
for _, file := range docFiles {
|
||||
docs = append(docs, LanguageDoc{
|
||||
Slug: file.Slug,
|
||||
Title: file.File(lang).Title,
|
||||
})
|
||||
}
|
||||
|
||||
return docs
|
||||
}
|
||||
|
||||
// SlugFromOldID gets a doc file's slug from its old ID
|
||||
func SlugFromOldID(i int) string {
|
||||
for _, d := range docFiles {
|
||||
if d.OldID == i {
|
||||
return d.Slug
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetFile retrieves a file, given a slug and a language.
|
||||
func GetFile(slug, language string) File {
|
||||
for _, f := range docFiles {
|
||||
if f.Slug != slug {
|
||||
continue
|
||||
}
|
||||
if val, ok := f.Languages[language]; ok {
|
||||
return val
|
||||
}
|
||||
return f.Languages[referenceLanguage]
|
||||
}
|
||||
return File{}
|
||||
}
|
||||
|
||||
var (
|
||||
ipMain = "163.172.71.251"
|
||||
ipMirror = "51.15.222.176"
|
||||
ipLastUpdated = time.Date(2018, 5, 13, 11, 45, 0, 0, time.UTC)
|
||||
ipRegex = regexp.MustCompile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$`)
|
||||
)
|
||||
|
||||
func updateIPs() {
|
||||
if time.Now().Sub(ipLastUpdated) < time.Hour*24*14 {
|
||||
return
|
||||
}
|
||||
ipLastUpdated = time.Now()
|
||||
|
||||
resp, err := http.Get("https://ip.ripple.moe")
|
||||
if err != nil {
|
||||
fmt.Println("error updating IPs", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Println("error updating IPs", err)
|
||||
return
|
||||
}
|
||||
|
||||
ips := strings.SplitN(string(data), "\n", 3)
|
||||
if len(ips) < 2 || !ipRegex.MatchString(ips[0]) || !ipRegex.MatchString(ips[1]) {
|
||||
return
|
||||
}
|
||||
ipMain = ips[0]
|
||||
ipMirror = ips[1]
|
||||
}
|
||||
|
||||
func init() {
|
||||
go updateIPs()
|
||||
}
|
137
modules/doc/loader.go
Normal file
137
modules/doc/loader.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Package doc handles documentation.
|
||||
package doc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// When we start the program, we should load the documentation files.
|
||||
err := loadDocFiles()
|
||||
if err != nil {
|
||||
fmt.Println("error while loading documentation:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// rawFile represents the data that may be provided at the top of files.
|
||||
type rawFile struct {
|
||||
Title string `yaml:"title"`
|
||||
OldID int `yaml:"old_id"`
|
||||
ReferenceVersion string `yaml:"reference_version"`
|
||||
}
|
||||
|
||||
func loadDocFiles() error {
|
||||
langs, err := loadLanguagesAvailable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir("website-docs/" + referenceLanguage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
data, err := ioutil.ReadFile("website-docs/" + referenceLanguage + "/" + file.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header := loadHeader(data)
|
||||
md5sum := fmt.Sprintf("%x", md5.Sum(data))
|
||||
|
||||
doc := Document{
|
||||
OldID: header.OldID,
|
||||
Slug: strings.TrimSuffix(file.Name(), ".md"),
|
||||
}
|
||||
|
||||
doc.Languages, err = loadLanguages(langs, file.Name(), md5sum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
docFiles = append(docFiles, doc)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadHeader(b []byte) rawFile {
|
||||
s := bufio.NewScanner(bytes.NewReader(b))
|
||||
var (
|
||||
isConf bool
|
||||
conf string
|
||||
)
|
||||
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
if !isConf {
|
||||
if line == "---" {
|
||||
isConf = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if line == "---" {
|
||||
break
|
||||
}
|
||||
conf += line + "\n"
|
||||
}
|
||||
|
||||
var f rawFile
|
||||
err := yaml.Unmarshal([]byte(conf), &f)
|
||||
if err != nil {
|
||||
fmt.Println("Error unmarshaling yaml:", err)
|
||||
return rawFile{}
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func loadLanguagesAvailable() ([]string, error) {
|
||||
files, err := ioutil.ReadDir("website-docs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
langs := make([]string, 0, len(files))
|
||||
for _, f := range files {
|
||||
if !f.IsDir() {
|
||||
continue
|
||||
}
|
||||
langs = append(langs, f.Name())
|
||||
}
|
||||
|
||||
return langs, nil
|
||||
}
|
||||
|
||||
func loadLanguages(langs []string, fname string, referenceMD5 string) (map[string]File, error) {
|
||||
m := make(map[string]File, len(langs))
|
||||
|
||||
for _, lang := range langs {
|
||||
data, err := ioutil.ReadFile("website-docs/" + lang + "/" + fname)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header := loadHeader(data)
|
||||
|
||||
m[lang] = File{
|
||||
IsUpdated: lang == referenceLanguage || header.ReferenceVersion == referenceMD5,
|
||||
Title: header.Title,
|
||||
referencesFile: "website-docs/" + lang + "/" + fname,
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
8
modules/fa-semantic-mappings/mappings.go
Normal file
8
modules/fa-semantic-mappings/mappings.go
Normal file
File diff suppressed because one or more lines are too long
62
modules/locale/lang_map.go
Normal file
62
modules/locale/lang_map.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package locale
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var languageMap = make(map[string]*po, 20)
|
||||
|
||||
func loadLanguages() {
|
||||
files, err := ioutil.ReadDir("./data/locales")
|
||||
if err != nil {
|
||||
fmt.Println("loadLanguages", err)
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name() == "templates.pot" || file.Name() == "." || file.Name() == ".." {
|
||||
continue
|
||||
}
|
||||
|
||||
p, err := parseFile("./data/locales/" + file.Name())
|
||||
if err != nil {
|
||||
fmt.Println(file.Name(), ":", err)
|
||||
continue
|
||||
}
|
||||
if p == nil {
|
||||
fmt.Println(file.Name(), ":", "p is nil")
|
||||
}
|
||||
|
||||
langName := strings.TrimPrefix(strings.TrimSuffix(file.Name(), ".po"), "templates-")
|
||||
languageMap[langName] = p
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
loadLanguages()
|
||||
}
|
||||
|
||||
// Get retrieves a string from a language
|
||||
func Get(langs []string, str string, vars ...interface{}) string {
|
||||
for _, lang := range langs {
|
||||
l := languageMap[lang]
|
||||
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if el := l.Translations[str]; el != "" {
|
||||
if len(vars) > 0 {
|
||||
return fmt.Sprintf(el, vars...)
|
||||
}
|
||||
return el
|
||||
}
|
||||
}
|
||||
|
||||
if len(vars) > 0 {
|
||||
return fmt.Sprintf(str, vars...)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
38
modules/locale/parse_header.go
Normal file
38
modules/locale/parse_header.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package locale
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseHeader parses an Accept-Language header, and sorts the values.
|
||||
func ParseHeader(header string) []string {
|
||||
if header == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(header, ",")
|
||||
|
||||
sort.Slice(parts, func(i, j int) bool {
|
||||
return getQuality(parts[i]) > getQuality(parts[j])
|
||||
})
|
||||
|
||||
for idx, val := range parts {
|
||||
parts[idx] = strings.Replace(strings.SplitN(val, ";q=", 2)[0], "-", "_", 1)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
func getQuality(s string) float32 {
|
||||
idx := strings.Index(s, ";q=")
|
||||
if idx == -1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(s[idx+3:], 32)
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
return float32(f)
|
||||
}
|
45
modules/locale/parse_header_test.go
Normal file
45
modules/locale/parse_header_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package locale
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseHeader(t *testing.T) {
|
||||
tt := []struct {
|
||||
In string
|
||||
Out []string
|
||||
}{
|
||||
{
|
||||
"en",
|
||||
[]string{"en"},
|
||||
},
|
||||
{
|
||||
"en-GB",
|
||||
[]string{"en_GB"},
|
||||
},
|
||||
{
|
||||
"en-GB;q=0.5,it",
|
||||
[]string{"it", "en_GB"},
|
||||
},
|
||||
{
|
||||
"en-GB;q=0.5,it,pl;q=0.2",
|
||||
[]string{"it", "en_GB", "pl"},
|
||||
},
|
||||
{
|
||||
"en-GB;q=0.5,pl;q=xd",
|
||||
[]string{"pl", "en_GB"},
|
||||
},
|
||||
{
|
||||
"",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, el := range tt {
|
||||
got := ParseHeader(el.In)
|
||||
if !reflect.DeepEqual(got, el.Out) {
|
||||
t.Errorf("got %v want %v", got, el.Out)
|
||||
}
|
||||
}
|
||||
}
|
112
modules/locale/parser.go
Normal file
112
modules/locale/parser.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package locale
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type po struct {
|
||||
Headers map[string]string
|
||||
Translations map[string]string
|
||||
}
|
||||
|
||||
// A minimal .po file parser.
|
||||
func parse(s *bufio.Scanner) *po {
|
||||
p := &po{
|
||||
Headers: make(map[string]string, 20),
|
||||
Translations: make(map[string]string, 500),
|
||||
}
|
||||
|
||||
const (
|
||||
currentNothing = iota
|
||||
currentMsgID
|
||||
currentMsgString
|
||||
)
|
||||
var current byte
|
||||
var currentID string
|
||||
var currentString string
|
||||
|
||||
for s.Scan() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "msgid "):
|
||||
unq, err := strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(line, "msgid")))
|
||||
if err != nil {
|
||||
fmt.Println(line)
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if current != currentNothing && currentID == "" && currentString != "" {
|
||||
for _, h := range strings.Split(currentString, "\n") {
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(h, ": ", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
p.Headers[parts[0]] = parts[1]
|
||||
}
|
||||
} else {
|
||||
p.Translations[currentID] = currentString
|
||||
}
|
||||
|
||||
currentID = unq
|
||||
current = currentMsgID
|
||||
case strings.HasPrefix(line, "msgstr "):
|
||||
unq, err := strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(line, "msgstr")))
|
||||
if err != nil {
|
||||
fmt.Println(line)
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
currentString = unq
|
||||
current = currentMsgString
|
||||
case strings.HasPrefix(line, "msgid_plural "), strings.HasPrefix(line, "msgstr["):
|
||||
// We currently don't support plural in the Go parser, at least hold it off 'til it's needed.
|
||||
current = currentNothing
|
||||
default:
|
||||
if current == currentNothing {
|
||||
continue
|
||||
}
|
||||
|
||||
unq, err := strconv.Unquote(strings.TrimSpace(line))
|
||||
if err != nil {
|
||||
fmt.Println(line)
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
switch current {
|
||||
case currentMsgID:
|
||||
currentID += unq
|
||||
case currentMsgString:
|
||||
currentString += unq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current == currentMsgString {
|
||||
p.Translations[currentID] = currentString
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func parseFile(fName string) (*po, error) {
|
||||
f, err := os.Open(fName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
s := bufio.NewScanner(f)
|
||||
p := parse(s)
|
||||
return p, nil
|
||||
}
|
3347
modules/top-passwords/top_passwords.go
Normal file
3347
modules/top-passwords/top_passwords.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user