Compare commits
25 Commits
a756c76631
...
94c8850e80
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | 94c8850e80 | |
Maxim Lebedev | aff4976a40 | |
Maxim Lebedev | 90a91c332d | |
Maxim Lebedev | 6fd916058d | |
Maxim Lebedev | f8a6580444 | |
Maxim Lebedev | 495d49dada | |
Maxim Lebedev | 43126b3d92 | |
Maxim Lebedev | 92b983f913 | |
Maxim Lebedev | 32f3b803e6 | |
Maxim Lebedev | 6eafdd4b86 | |
Maxim Lebedev | ae9f1cae4a | |
Maxim Lebedev | 516758b74a | |
Maxim Lebedev | d762f33b19 | |
Maxim Lebedev | fe8a7d36de | |
Maxim Lebedev | 7087380d15 | |
Maxim Lebedev | 2ad44a03bc | |
Maxim Lebedev | f3eca3dc73 | |
Maxim Lebedev | c9db0f0ce0 | |
Maxim Lebedev | 2c23c74118 | |
Maxim Lebedev | d8673188ce | |
Maxim Lebedev | 6046187817 | |
Maxim Lebedev | 9cdb470ada | |
Maxim Lebedev | c83a86119e | |
Maxim Lebedev | 66dadfe9c3 | |
Maxim Lebedev | 104814a633 |
|
@ -23,7 +23,7 @@ WORKDIR /
|
|||
|
||||
COPY --from=builder /app/home /home
|
||||
|
||||
VOLUME ["/content", "/theme"]
|
||||
VOLUME ["/content", "/theme", "/static"]
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ Four options are available:
|
|||
* `HOME_CONTENT_DIR` -- the name or path of the directory that contains pages and static files, the default is `content/`;
|
||||
* `HOME_HOST` -- IP address where the engine will run, default is `0.0.0.0.0`;
|
||||
* `HOME_THEME_DIR` -- the name or path of the directory that contains the template files, default is `theme/`;
|
||||
* `HOME_STATIC_DIR` -- name or path of the directory containing publicly accessible files from any part of the site "as is", default is `static/`;
|
||||
* `HOME_PORT` -- the port on which the engine will work, by default is `3000`;
|
||||
|
||||
Configuration of the site and its pages is fully provided through FrontMatter in Markdown files located in `$HOME_CONTENT_DIR`.
|
||||
|
@ -41,9 +42,14 @@ backlink: "https://mstdn.io/@toby3d"
|
|||
Pages are published instantly through the creation of Markdown files. Index files of the home page and sections are called `index.md` or `index.lang.md` where `lang` is a two-letter designation of the localization language. The engine supports working with any number of localizations of the same pages: `index.ru.md`, `index.en.md`, `index.jp.md` and so on.
|
||||
|
||||
Individual pages can be named anything, the name will be used as a slug from the URL, e.g.:
|
||||
* `$HOME_CONTENT_DIR/folder/file.md`: `/folder/file`, `/folder/file/`, `/folder/file/index.html` and `/folder/file.html`;
|
||||
* `$HOME_CONTENT_DIR/now/index.md`: `/now`, `/now/`, `/now/index.html` and `/now.html`;
|
||||
* `$HOME_CONTENT_DIR/2023/11/09.md`: `/2023/11/09`, `/2023/11/09/`, `/2023/11/09/index.html` and `/2023/11/09.html`;
|
||||
* `$HOME_CONTENT_DIR/folder/file.md`:
|
||||
* `/folder/file`
|
||||
* `/folder/file/`
|
||||
* `/folder/file.html`
|
||||
* `$HOME_CONTENT_DIR/now/index.md`:
|
||||
* `/now`
|
||||
* `/now/`
|
||||
* `/now/index.html`
|
||||
* and so on;
|
||||
|
||||
The rules on localizations also apply here: `folder/file.ru.md` b `folder/file.en.md`, `now.jp.md`, `2023/11/09.de.md` and so on.
|
||||
|
@ -53,6 +59,7 @@ The content of the published pages also conforms to the FrontMatter format:
|
|||
---
|
||||
# Known properties:
|
||||
title: "Hello, World!"
|
||||
description: "Demo page for demo"
|
||||
|
||||
# Any other key-value sets will be treated as secondary parameters and accessed through the `.Page.Params`:
|
||||
style:
|
||||
|
@ -67,7 +74,9 @@ contact: "hey@toby3d.me"
|
|||
This is a sample page content for demo.
|
||||
```
|
||||
|
||||
Static files like images, videos, `robots.txt`, styles, scripts and anything else are placed in `$HOME_CONTENT_DIR` as they are: in the root if they should be available globally via `.Site.Files` and for home pages, as well as in the necessary directories with pages to access them via `.Page.Files`. The format and filenames are not restricted in any way.
|
||||
Any static files can be hosted and published in two ways:
|
||||
* In `$HOME_STATIC_DIR` as publicly available static, "as is" without any restrictions, in the same path in which the file is located in the directory;
|
||||
* In `$HOME_CONTENT_DIR` next to the specific page: the file will be available in the `.Site.Resources` and `.Page.Resources` collections and restricted by the access settings of the parent page;
|
||||
|
||||
Templates are required to render page content in browsers and must be placed in `$HOME_THEME_DIR`. At least one template with a `baseof` block that will be called for each page is required, for example:
|
||||
```html
|
||||
|
@ -75,7 +84,9 @@ Templates are required to render page content in browsers and must be placed in
|
|||
|
||||
{{ define "baseof" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ .Site.Language }}">
|
||||
<html lang="{{ or .Page.Language.Lang .Site.Language.Lang }}"
|
||||
dir="{{ or .Page.Language.Dir .Site.Language.Dir }}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
|
@ -84,12 +95,12 @@ Templates are required to render page content in browsers and must be placed in
|
|||
<title>{{ .Site.Title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{ block "body" . }}{{ .Page.Content }}{{ end }}
|
||||
{{ block "body" . }}{{ .Page.Content | transform.Markdownify }}{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
Templating works according to [the instructions of the standard Go templating engine](https://pkg.go.dev/html/template) with minor additions in the form of additional utilities. In `$HOME_THEME_DIR`, any combination and hierarchy of templates and their relationships is allowed as long as there is a `baseof` as a single parent block.
|
||||
Templating works according to [the instructions of the standard Go templating engine](https://pkg.go.dev/html/template) with minor additions in the form of [additional utilities](../internal/templateutil/templateutil.go). In `$HOME_THEME_DIR`, any combination and hierarchy of templates and their relationships is allowed as long as there is a `baseof` as a single parent block.
|
||||
|
||||
Two structures are thrown into each template as contexts: `.Site` with global options from `$HOME_CONTENT_DIR/index.md` and the currently viewed page `.Page` with its options found in `$HOME_CONTENT_DIR`. The data structures in these contexts can be found in the [domains](../internal/domain) package, they are used in the templates as is.
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
* `HOME_CONTENT_DIR` -- имя или путь директории в которой содержатся страницы и статика, по-умолчанию `content/`;
|
||||
* `HOME_HOST` -- IP-адрес на котором будет работать движок, по-умолчанию `0.0.0.0`;
|
||||
* `HOME_THEME_DIR` -- имя или путь директории в которой содержатся файлы шаблонов, по-умолчанию `theme/`;
|
||||
* `HOME_STATIC_DIR` -- имя или путь директории в которой содержатся публично доступные файлы из любой части сайта "как есть", по-умолчанию `static/`;
|
||||
* `HOME_PORT` -- порт на котором будет работать движок, по-умолчанию `3000`;
|
||||
|
||||
Конфигурация сайта и его страниц полностью обеспечивается через FrontMatter в Markdown-файлах находящиеся в `$HOME_CONTENT_DIR`.
|
||||
|
@ -41,9 +42,14 @@ backlink: "https://mstdn.io/@toby3d"
|
|||
Страницы публикуются мгновенно через создание Markdown-файлов. Индексные файлы главной страницы и директорий называются `index.md` или `index.lang.md` где `lang` это двубуквенное обозначение языка локализации. Движок поддерживает работу с любым числом локализаций одних и тех же страниц: `index.ru.md`, `index.en.md`, `index.jp.md` и так далее.
|
||||
|
||||
Именованные страницы могут называться как угодно, имя будет использоваться в качестве slug из URL, например:
|
||||
* `$HOME_CONTENT_DIR/folder/file.md`: `/folder/file`, `/folder/file/`, `/folder/file/index.html` и `/folder/file.html`;
|
||||
* `$HOME_CONTENT_DIR/now/index.md`: `/now`, `/now/`, `/now/index.html` и `/now.html`;
|
||||
* `$HOME_CONTENT_DIR/2023/11/09.md`: `/2023/11/09`, `/2023/11/09/`, `/2023/11/09/index.html` и `/2023/11/09.html`;
|
||||
* `$HOME_CONTENT_DIR/folder/file.md`:
|
||||
* `/folder/file`
|
||||
* `/folder/file/`
|
||||
* `/folder/file.html`
|
||||
* `$HOME_CONTENT_DIR/now/index.md`:
|
||||
* `/now`
|
||||
* `/now/`
|
||||
* `/now/index.html`
|
||||
* и так далее;
|
||||
|
||||
Правила по локализациям здесь также применимы: `folder/file.ru.md` b `folder/file.en.md`, `now.jp.md`, `2023/11/09.de.md` и так далее.
|
||||
|
@ -53,6 +59,7 @@ backlink: "https://mstdn.io/@toby3d"
|
|||
---
|
||||
# Известные параметры:
|
||||
title: "Hello, World!"
|
||||
description: "Demo page for demo"
|
||||
|
||||
# Любые другие наборы ключей-значений будут восприниматься как второстепенные параметры и доступные через `.Page.Params`:
|
||||
style:
|
||||
|
@ -67,7 +74,9 @@ contact: "hey@toby3d.me"
|
|||
This is a sample page content for demo.
|
||||
```
|
||||
|
||||
Статические файлы вроде изображений, видео, `robots.txt`, стилей, скриптов и чего-угодно ещё размещаются в `$HOME_CONTENT_DIR` как есть: в корень если они должны быть доступны глобально через `.Site.Files` и для домашних страниц, а также в нужных директориях со страницами для доступа к ним через `.Page.Files`. Формат и имена файлов ничем не ограничены.
|
||||
Любые статические файлы могут быть размещены и опубликованы двумя способами:
|
||||
* В `$HOME_STATIC_DIR` как публично доступная статика, без каких-либо ограничений "как есть", по тому же пути в котором файл расположен в директории;
|
||||
* В `$HOME_CONTENT_DIR` рядом с нужной страницей: файл будет доступен в коллекциях `.Site.Resources` и `.Page.Resources` и ограничен настройками доступа родительской страницы;
|
||||
|
||||
Для рендера содержимого страниц в браузерах необходимы шаблоны, которые должны размещаться в `$HOME_THEME_DIR`. Для работы необходим хотя бы один шаблон с блоком `baseof` который будет вызываться для каждой страницы, например:
|
||||
```html
|
||||
|
@ -75,7 +84,9 @@ This is a sample page content for demo.
|
|||
|
||||
{{ define "baseof" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ .Site.Language }}">
|
||||
<html lang="{{ or .Page.Language.Lang .Site.Language.Lang }}"
|
||||
dir="{{ or .Page.Language.Dir .Site.Language.Dir }}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
|
@ -84,12 +95,12 @@ This is a sample page content for demo.
|
|||
<title>{{ .Site.Title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{ block "body" . }}{{ .Page.Content }}{{ end }}
|
||||
{{ block "body" . }}{{ .Page.Content | transform.Markdownify }}{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
Шаблонизация работает по [инструкциям стандартного шаблонизатора Go](https://pkg.go.dev/html/template) с небольшими дополнениями в виде дополнительных утилит. В `$HOME_THEME_DIR` разрешена любая комбинация и иеархия шаблонов и их взаимосвязей до тех пор, пока существует `baseof` в качестве единого родительского блока.
|
||||
Шаблонизация работает по [инструкциям стандартного шаблонизатора Go](https://pkg.go.dev/html/template) с небольшими дополнениями в виде [дополнительных утилит](../internal/templateutil/templateutil.go). В `$HOME_THEME_DIR` разрешена любая комбинация и иеархия шаблонов и их взаимосвязей до тех пор, пока существует `baseof` в качестве единого родительского блока.
|
||||
|
||||
В качестве контекста в каждый шаблон пробрасываются две структуры: `.Site` с глобальными опциям из `$HOME_CONTENT_DIR/index.md` и текущая просматриваемая страница `.Page` с её опциями найденная в `$HOME_CONTENT_DIR`. Структуры данных в этих контекстах можно посмотреть в пакете [domains](../internal/domain), они используются в шаблонах как есть.
|
||||
|
|
|
@ -11,6 +11,7 @@ type Config struct {
|
|||
ContentDir string `env:"CONTENT_DIR" envDefault:"content/"`
|
||||
Host string `env:"HOST" envDefault:"0.0.0.0"`
|
||||
ThemeDir string `env:"THEME_DIR" envDefault:"theme/"`
|
||||
StaticDir string `env:"STATIC_DIR" envDefault:"static/"`
|
||||
Port uint16 `env:"PORT" envDefault:"3000"`
|
||||
}
|
||||
|
||||
|
@ -21,6 +22,7 @@ func TestConfig(tb testing.TB) *Config {
|
|||
ContentDir: "testdata/content/",
|
||||
Host: "0.0.0.0",
|
||||
ThemeDir: "testdata/theme/",
|
||||
StaticDir: "testdata/static/",
|
||||
Port: 3000,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,10 @@ import (
|
|||
"crypto/md5"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
language language.Tag
|
||||
Language Language
|
||||
baseFileName string
|
||||
contentBaseName string
|
||||
dir string
|
||||
|
@ -23,7 +21,7 @@ type File struct {
|
|||
|
||||
func NewFile(path string) File {
|
||||
out := File{
|
||||
language: language.Tag{},
|
||||
Language: LanguageUnd,
|
||||
baseFileName: "",
|
||||
contentBaseName: "",
|
||||
dir: filepath.Dir(path) + "/",
|
||||
|
@ -38,7 +36,7 @@ func NewFile(path string) File {
|
|||
out.baseFileName = strings.TrimSuffix(out.logicalName, filepath.Ext(out.logicalName))
|
||||
|
||||
parts := strings.Split(out.baseFileName, ".")
|
||||
out.language = language.Make(parts[len(parts)-1])
|
||||
out.Language = NewLanguage(parts[len(parts)-1])
|
||||
out.translationBaseName = strings.Join(parts[:len(parts)-1], ".")
|
||||
out.contentBaseName = out.translationBaseName
|
||||
|
||||
|
@ -79,12 +77,6 @@ func (f File) Filename() string {
|
|||
return f.filename
|
||||
}
|
||||
|
||||
// Language returns language.Tag of current file based on his suffix before
|
||||
// extention.
|
||||
func (f File) Language() language.Tag {
|
||||
return f.language
|
||||
}
|
||||
|
||||
func (f File) LogicalName() string {
|
||||
return f.logicalName
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
|
@ -106,7 +104,7 @@ func TestFile_Ext(t *testing.T) {
|
|||
func TestFile_Language(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var expect language.Tag = language.English
|
||||
var expect domain.Language = domain.NewLanguage("en")
|
||||
|
||||
for name, input := range map[string]string{
|
||||
"regular": testRegularFile,
|
||||
|
@ -118,7 +116,7 @@ func TestFile_Language(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewFile(input).Language(); actual != expect {
|
||||
if actual := domain.NewFile(input).Language; actual != expect {
|
||||
t.Errorf("Language() = '%s', want '%s'", actual, expect)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/language/display"
|
||||
)
|
||||
|
||||
type Language struct {
|
||||
code string
|
||||
lang string
|
||||
name string
|
||||
dir string
|
||||
}
|
||||
|
||||
var LanguageUnd Language = Language{}
|
||||
|
||||
func NewLanguage(raw string) Language {
|
||||
tag, err := language.BCP47.Parse(raw)
|
||||
if err != nil || tag == language.Und {
|
||||
return LanguageUnd
|
||||
}
|
||||
|
||||
out := Language{
|
||||
code: tag.String(),
|
||||
dir: "ltr",
|
||||
name: strings.ToLower(display.Self.Name(tag)),
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case language.Arabic, language.Persian, language.Hebrew, language.Urdu:
|
||||
out.dir = "rtl"
|
||||
}
|
||||
|
||||
base, _ := tag.Base()
|
||||
out.lang = base.String()
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (l Language) Lang() string {
|
||||
return l.lang
|
||||
}
|
||||
|
||||
func (l Language) Code() string {
|
||||
return l.code
|
||||
}
|
||||
|
||||
func (l Language) Dir() string {
|
||||
return l.dir
|
||||
}
|
||||
|
||||
func (l Language) Name() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l Language) String() string {
|
||||
if l.code == "" {
|
||||
return l.code
|
||||
}
|
||||
|
||||
return "und"
|
||||
}
|
||||
|
||||
func (l Language) GoString() string {
|
||||
return "domain.Language(" + l.String() + ")"
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package domain_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
func TestLanguage_Lang(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
"2letter": {"en", "en"},
|
||||
"3letter": {"eng", "en"},
|
||||
"region": {"en-US", "en"},
|
||||
"und": {"", ""},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewLanguage(tc.input).Lang(); actual != tc.expect {
|
||||
t.Errorf("Lang() = %s, want %s", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLanguage_Code(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
"2letter": {"en", "en"},
|
||||
"3letter": {"eng", "en"},
|
||||
"region": {"en-US", "en-US"},
|
||||
"und": {"", ""},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewLanguage(tc.input).Code(); actual != tc.expect {
|
||||
t.Errorf("Code() = %s, want %s", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLanguage_Dir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
"2letter": {"en", "ltr"},
|
||||
"rtl": {"ur", "rtl"},
|
||||
"3letter": {"eng", "ltr"},
|
||||
"region": {"en-US", "ltr"},
|
||||
"und": {"", ""},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewLanguage(tc.input).Dir(); actual != tc.expect {
|
||||
t.Errorf("Dir() = %s, want %s", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLanguage_Name(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
"localized": {"ru", "русский"},
|
||||
"2letter": {"en", "english"},
|
||||
"3letter": {"eng", "english"},
|
||||
"region": {"en-US", "american english"},
|
||||
"und": {"", ""},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewLanguage(tc.input).Name(); actual != tc.expect {
|
||||
t.Errorf("Name() = %s, want %s", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
package domain
|
||||
|
||||
import "golang.org/x/text/language"
|
||||
|
||||
type Page struct {
|
||||
Language language.Tag
|
||||
Language Language
|
||||
Params map[string]any
|
||||
File File
|
||||
Description string
|
||||
|
|
|
@ -16,6 +16,8 @@ import (
|
|||
)
|
||||
|
||||
type Resource struct {
|
||||
File File
|
||||
|
||||
modTime time.Time
|
||||
reader io.ReadSeeker
|
||||
params map[string]any // TODO(toby3d): set from Page configuration
|
||||
|
@ -30,6 +32,7 @@ type Resource struct {
|
|||
func NewResource(modTime time.Time, content []byte, key string) *Resource {
|
||||
mediaType, _, _ := mime.ParseMediaType(mime.TypeByExtension(path.Ext(key)))
|
||||
out := &Resource{
|
||||
File: NewFile(key),
|
||||
modTime: modTime,
|
||||
key: key,
|
||||
name: key, // TODO(toby3d): set from Page configuration
|
||||
|
|
|
@ -2,17 +2,30 @@ package domain
|
|||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type Site struct {
|
||||
Language language.Tag
|
||||
BaseURL *url.URL
|
||||
Params map[string]any
|
||||
TimeZone *time.Location
|
||||
File File
|
||||
Title string
|
||||
Resources Resources
|
||||
DefaultLanguage Language
|
||||
Language Language
|
||||
Languages []Language
|
||||
BaseURL *url.URL
|
||||
Params map[string]any
|
||||
TimeZone *time.Location
|
||||
File File
|
||||
Title string
|
||||
Resources Resources
|
||||
}
|
||||
|
||||
func (s Site) LanguagePrefix() string {
|
||||
if s.Language != LanguageUnd {
|
||||
return path.Join("/", s.Language.lang, "/")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s Site) IsMultiLingual() bool {
|
||||
return 1 < len(s.Languages)
|
||||
}
|
||||
|
|
|
@ -3,11 +3,9 @@ package page
|
|||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Get(ctx context.Context, lang language.Tag, path string) (*domain.Page, error)
|
||||
Get(ctx context.Context, lang domain.Language, path string) (*domain.Page, error)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"io/fs"
|
||||
|
||||
"github.com/adrg/frontmatter"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
|
@ -22,8 +21,7 @@ type (
|
|||
}
|
||||
|
||||
fileSystemPageRepository struct {
|
||||
dir fs.FS
|
||||
rootPath string
|
||||
dir fs.FS
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -31,17 +29,16 @@ var FrontMatterFormats = []*frontmatter.Format{
|
|||
frontmatter.NewFormat(`---`, `---`, yaml.Unmarshal),
|
||||
}
|
||||
|
||||
func NewFileSystemPageRepository(rootDir fs.FS) page.Repository {
|
||||
func NewFileSystemPageRepository(dir fs.FS) page.Repository {
|
||||
return &fileSystemPageRepository{
|
||||
dir: rootDir,
|
||||
dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag, p string) (*domain.Page, error) {
|
||||
func (repo *fileSystemPageRepository) Get(ctx context.Context, lang domain.Language, p string) (*domain.Page, error) {
|
||||
ext := ".md"
|
||||
if lang != language.Und {
|
||||
base, _ := lang.Base()
|
||||
ext = "." + base.String() + ext
|
||||
if lang != domain.LanguageUnd {
|
||||
ext = "." + lang.Lang() + ext
|
||||
}
|
||||
|
||||
target := p + ext
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"testing/fstest"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
repository "source.toby3d.me/toby3d/home/internal/page/repository/fs"
|
||||
|
@ -34,41 +33,46 @@ func TestGet(t *testing.T) {
|
|||
"index": {
|
||||
input: path.Join("index"),
|
||||
expect: &domain.Page{
|
||||
Content: []byte("index.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Content: []byte("index.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Translations: make([]*domain.Page, 0),
|
||||
},
|
||||
},
|
||||
"file": {
|
||||
input: path.Join("file"),
|
||||
expect: &domain.Page{
|
||||
Content: []byte("file.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Content: []byte("file.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Translations: make([]*domain.Page, 0),
|
||||
},
|
||||
},
|
||||
"folder": {
|
||||
input: path.Join("folder", "index"),
|
||||
expect: &domain.Page{
|
||||
Content: []byte("folder/index.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Content: []byte("folder/index.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Translations: make([]*domain.Page, 0),
|
||||
},
|
||||
},
|
||||
"both-file": {
|
||||
input: path.Join("both"),
|
||||
expect: &domain.Page{
|
||||
Content: []byte("both.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Content: []byte("both.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Translations: make([]*domain.Page, 0),
|
||||
},
|
||||
},
|
||||
"both-folder": {
|
||||
input: path.Join("both", "index"),
|
||||
expect: &domain.Page{
|
||||
Content: []byte("both/index.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Content: []byte("both/index.md"),
|
||||
Params: make(map[string]any),
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Translations: make([]*domain.Page, 0),
|
||||
},
|
||||
},
|
||||
} {
|
||||
|
@ -77,12 +81,19 @@ func TestGet(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
out, err := repo.Get(context.Background(), language.Und, tc.input)
|
||||
out, err := repo.Get(context.Background(), domain.LanguageUnd, tc.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(out, tc.expect, cmp.AllowUnexported(language.Und)); diff != "" {
|
||||
if diff := cmp.Diff(out, tc.expect, cmp.FilterPath(func(p cmp.Path) bool {
|
||||
switch p.String() {
|
||||
default:
|
||||
return false
|
||||
case "File", "Language":
|
||||
return true
|
||||
}
|
||||
}, cmp.Ignore())); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -4,13 +4,11 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
Do(ctx context.Context, lang language.Tag, path string) (*domain.Page, error)
|
||||
Do(ctx context.Context, lang domain.Language, path string) (*domain.Page, error)
|
||||
}
|
||||
|
||||
var ErrNotExist error = errors.New("page not exists")
|
||||
|
|
|
@ -4,9 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/page"
|
||||
|
@ -26,7 +23,7 @@ func NewPageUseCase(pages page.Repository, statics static.Repository) page.UseCa
|
|||
}
|
||||
}
|
||||
|
||||
func (ucase *pageUseCase) Do(ctx context.Context, lang language.Tag, p string) (*domain.Page, error) {
|
||||
func (ucase *pageUseCase) Do(ctx context.Context, lang domain.Language, p string) (*domain.Page, error) {
|
||||
ext := path.Ext(p)
|
||||
if ext == ".html" {
|
||||
p = p[:len(p)-len(ext)]
|
||||
|
@ -55,22 +52,11 @@ func (ucase *pageUseCase) Do(ctx context.Context, lang language.Tag, p string) (
|
|||
}
|
||||
|
||||
for _, res := range out.Resources.GetType(domain.ResourceTypePage) {
|
||||
// TODO(toby3d): simplify this, it's awful
|
||||
resName := path.Base(res.Key())
|
||||
resExt := path.Ext(resName)
|
||||
resParts := strings.Split(resName[:len(resName)-len(resExt)], ".")
|
||||
|
||||
translationBaseName := strings.Join(resParts[:len(resParts)-1], ".")
|
||||
if translationBaseName != out.File.TranslationBaseName() {
|
||||
if res.File.TranslationBaseName() != out.File.TranslationBaseName() {
|
||||
continue
|
||||
}
|
||||
|
||||
resLang := language.Make(resParts[len(resParts)-1])
|
||||
if resLang == language.Und {
|
||||
continue
|
||||
}
|
||||
|
||||
translation, err := ucase.pages.Get(ctx, resLang, targets[i])
|
||||
translation, err := ucase.pages.Get(ctx, res.File.Language, targets[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
@ -78,11 +64,6 @@ func (ucase *pageUseCase) Do(ctx context.Context, lang language.Tag, p string) (
|
|||
out.Translations = append(out.Translations, translation)
|
||||
}
|
||||
|
||||
translations := make([]string, 0)
|
||||
for i := range out.Translations {
|
||||
translations = append(translations, out.Translations[i].Language.String())
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"testing/fstest"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
|
||||
"source.toby3d.me/toby3d/home/internal/page/usecase"
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
|
@ -49,7 +49,7 @@ func TestDo(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual, err := ucase.Do(context.Background(), language.Und, tc.input)
|
||||
actual, err := ucase.Do(context.Background(), domain.LanguageUnd, tc.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -3,11 +3,9 @@ package site
|
|||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Get(ctx context.Context, lang language.Tag) (*domain.Site, error)
|
||||
Get(ctx context.Context, lang domain.Language) (*domain.Site, error)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/adrg/frontmatter"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
|
@ -17,11 +16,12 @@ import (
|
|||
|
||||
type (
|
||||
Site struct {
|
||||
Title string `yaml:"title"`
|
||||
TimeZone TimeZone `yaml:"timeZone"`
|
||||
BaseURL URL `yaml:"baseUrl"`
|
||||
Params map[string]any `yaml:",inline"`
|
||||
Content []byte `yaml:"-"`
|
||||
Title string `yaml:"title"`
|
||||
TimeZone TimeZone `yaml:"timeZone"`
|
||||
DefaultLanguage Language `yaml:"defaultLanguage"`
|
||||
BaseURL URL `yaml:"baseUrl"`
|
||||
Params map[string]any `yaml:",inline"`
|
||||
Content []byte `yaml:"-"`
|
||||
}
|
||||
|
||||
TimeZone struct {
|
||||
|
@ -32,9 +32,12 @@ type (
|
|||
*url.URL `yaml:"-"`
|
||||
}
|
||||
|
||||
Language struct {
|
||||
domain.Language `yaml:"-"`
|
||||
}
|
||||
|
||||
fileSystemSiteRepository struct {
|
||||
dir fs.FS
|
||||
rootPath string
|
||||
dir fs.FS
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -48,11 +51,10 @@ func NewFileSystemSiteRepository(rootDir fs.FS) site.Repository {
|
|||
}
|
||||
}
|
||||
|
||||
func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang language.Tag) (*domain.Site, error) {
|
||||
func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang domain.Language) (*domain.Site, error) {
|
||||
ext := ".md"
|
||||
if lang != language.Und {
|
||||
base, _ := lang.Base()
|
||||
ext = "." + base.String() + ext
|
||||
if lang != domain.LanguageUnd {
|
||||
ext = "." + lang.Lang() + ext
|
||||
}
|
||||
|
||||
target := "index" + ext
|
||||
|
@ -69,12 +71,14 @@ func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang language.Tag
|
|||
}
|
||||
|
||||
return &domain.Site{
|
||||
File: domain.NewFile(target),
|
||||
Language: lang,
|
||||
Title: data.Title,
|
||||
BaseURL: data.BaseURL.URL,
|
||||
TimeZone: data.TimeZone.Location,
|
||||
Params: data.Params,
|
||||
File: domain.NewFile(target),
|
||||
DefaultLanguage: data.DefaultLanguage.Language,
|
||||
Language: lang,
|
||||
Title: data.Title,
|
||||
BaseURL: data.BaseURL.URL,
|
||||
TimeZone: data.TimeZone.Location,
|
||||
Params: data.Params,
|
||||
Languages: make([]domain.Language, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -126,6 +130,24 @@ func (u URL) MarshalYAML() (any, error) {
|
|||
return u.URL.String(), nil
|
||||
}
|
||||
|
||||
func (l *Language) UnmarshalYAML(value *yaml.Node) error {
|
||||
if value.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Language = domain.NewLanguage(value.Value)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Language) MarshalYAML() (any, error) {
|
||||
if l.Language == domain.LanguageUnd {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return l.Language.Lang(), nil
|
||||
}
|
||||
|
||||
func NewSite() *Site {
|
||||
return &Site{
|
||||
Params: make(map[string]any),
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"testing/fstest"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
repository "source.toby3d.me/toby3d/home/internal/site/repository/fs"
|
||||
|
@ -15,6 +14,8 @@ import (
|
|||
func TestGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testLangEn := domain.NewLanguage("en")
|
||||
testLangRu := domain.NewLanguage("ru")
|
||||
testData := fstest.MapFS{
|
||||
"index.ru.md": &fstest.MapFile{
|
||||
Data: []byte("---\ntitle: пример\n---\nпривет, мир!\n"),
|
||||
|
@ -27,23 +28,25 @@ func TestGet(t *testing.T) {
|
|||
repo := repository.NewFileSystemSiteRepository(testData)
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input language.Tag
|
||||
expect *domain.Site
|
||||
input domain.Language
|
||||
}{
|
||||
"english": {
|
||||
input: language.English,
|
||||
input: testLangEn,
|
||||
expect: &domain.Site{
|
||||
Language: language.English,
|
||||
Title: "example",
|
||||
Params: make(map[string]any),
|
||||
Language: testLangEn,
|
||||
Title: "example",
|
||||
Params: make(map[string]any),
|
||||
Languages: make([]domain.Language, 0),
|
||||
},
|
||||
},
|
||||
"russian": {
|
||||
input: language.Russian,
|
||||
input: testLangRu,
|
||||
expect: &domain.Site{
|
||||
Language: language.Russian,
|
||||
Title: "пример",
|
||||
Params: make(map[string]any),
|
||||
Language: testLangRu,
|
||||
Title: "пример",
|
||||
Params: make(map[string]any),
|
||||
Languages: make([]domain.Language, 0),
|
||||
},
|
||||
},
|
||||
} {
|
||||
|
@ -57,7 +60,14 @@ func TestGet(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(out, tc.expect, cmp.AllowUnexported(language.Und)); diff != "" {
|
||||
if diff := cmp.Diff(out, tc.expect, cmp.FilterPath(func(p cmp.Path) bool {
|
||||
switch p.String() {
|
||||
default:
|
||||
return false
|
||||
case "File", "Language", "DefaultLanguage":
|
||||
return true
|
||||
}
|
||||
}, cmp.Ignore())); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,11 +3,9 @@ package site
|
|||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
Do(ctx context.Context, lang language.Tag) (*domain.Site, error)
|
||||
Do(ctx context.Context, lang domain.Language) (*domain.Site, error)
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
|
@ -23,13 +21,17 @@ func NewSiteUseCase(sites site.Repository, statics static.Repository) site.UseCa
|
|||
}
|
||||
}
|
||||
|
||||
func (ucase *siteUseCase) Do(ctx context.Context, lang language.Tag) (*domain.Site, error) {
|
||||
out, err := ucase.sites.Get(ctx, language.Und)
|
||||
func (ucase *siteUseCase) Do(ctx context.Context, lang domain.Language) (*domain.Site, error) {
|
||||
out, err := ucase.sites.Get(ctx, domain.LanguageUnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find base site data: %w", err)
|
||||
}
|
||||
|
||||
out.Resources, _, _ = ucase.statics.Fetch(ctx, "")
|
||||
if out.Resources, _, err = ucase.statics.Fetch(ctx, ""); err == nil {
|
||||
for _, res := range out.Resources.Match("index.*.md") {
|
||||
out.Languages = append(out.Languages, res.File.Language)
|
||||
}
|
||||
}
|
||||
|
||||
sub, err := ucase.sites.Get(ctx, lang)
|
||||
if err != nil {
|
||||
|
|
|
@ -7,8 +7,7 @@ import (
|
|||
"net/url"
|
||||
"path"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
)
|
||||
|
||||
|
@ -25,7 +24,7 @@ func New(siter site.UseCase) *Namespace {
|
|||
}
|
||||
|
||||
func (ns *Namespace) AbsURL(p string) (string, error) {
|
||||
site, err := ns.siter.Do(context.Background(), language.Und)
|
||||
site, err := ns.siter.Do(context.Background(), domain.LanguageUnd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot fetch site root for AbsURL processing: %w", err)
|
||||
}
|
||||
|
|
41
main.go
41
main.go
|
@ -63,20 +63,23 @@ var cpuProfilePath, memProfilePath string
|
|||
func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
|
||||
themeDir := os.DirFS(config.ThemeDir)
|
||||
contentDir := os.DirFS(config.ContentDir)
|
||||
statics := staticfsrepo.NewFileServerStaticRepository(contentDir)
|
||||
resources := staticfsrepo.NewFileServerStaticRepository(contentDir)
|
||||
sites := sitefsrepo.NewFileSystemSiteRepository(contentDir)
|
||||
siter := siteucase.NewSiteUseCase(sites, statics)
|
||||
siter := siteucase.NewSiteUseCase(sites, resources)
|
||||
|
||||
funcMap, err := templateutil.New(themeDir, siter)
|
||||
if err != nil {
|
||||
logger.Fatalln("cannot setup template.FuncMap for templates: %w", err)
|
||||
}
|
||||
|
||||
staticer := staticucase.NewStaticUseCase(statics)
|
||||
staticDir := os.DirFS(config.StaticDir)
|
||||
statics := staticfsrepo.NewFileServerStaticRepository(staticDir)
|
||||
// TODO(toby3d): use exists static use case or split that on static and resource modules?
|
||||
resourcer := staticucase.NewStaticUseCase(resources)
|
||||
themes := themefsrepo.NewFileSystemThemeRepository(themeDir, funcMap)
|
||||
themer := themeucase.NewThemeUseCase(themes)
|
||||
pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
|
||||
pager := pageucase.NewPageUseCase(pages, statics)
|
||||
pager := pageucase.NewPageUseCase(pages, resources)
|
||||
|
||||
matcher := language.NewMatcher(message.DefaultCatalog.Languages())
|
||||
|
||||
|
@ -98,26 +101,18 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
|
|||
return
|
||||
}
|
||||
|
||||
lang, err := language.Parse(head)
|
||||
if err != nil || lang == language.Und {
|
||||
res, err := staticer.Do(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), res)
|
||||
// TODO(toby3d): use exists static use case or split that on static and resource modules?
|
||||
// INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it
|
||||
// first before deep down to any page or it's resource which might be secured by middleware or
|
||||
// something else.
|
||||
static, err := statics.Get(r.Context(), strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if err == nil {
|
||||
http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
lang := domain.NewLanguage(head)
|
||||
r.URL.Path = tail
|
||||
|
||||
s, err := siter.Do(r.Context(), lang)
|
||||
|
@ -135,7 +130,7 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
|
|||
return
|
||||
}
|
||||
|
||||
res, err := staticer.Do(r.Context(), r.URL.Path)
|
||||
res, err := resourcer.Do(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
|
@ -155,8 +150,7 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
|
|||
|
||||
contentLanguage := make([]string, len(p.Translations))
|
||||
for i := range p.Translations {
|
||||
base, _ := p.Translations[i].Language.Base()
|
||||
contentLanguage[i] = base.String()
|
||||
contentLanguage[i] = p.Translations[i].Language.Code()
|
||||
}
|
||||
|
||||
w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", "))
|
||||
|
@ -220,6 +214,7 @@ func main() {
|
|||
for _, dir := range []*string{
|
||||
&config.ContentDir,
|
||||
&config.ThemeDir,
|
||||
&config.StaticDir,
|
||||
} {
|
||||
if *dir, err = filepath.Abs(filepath.Clean(*dir)); err != nil {
|
||||
logger.Fatalf("cannot format '%s' into absolute path: %s", *dir, err)
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package display
|
||||
|
||||
// This file contains sets of data for specific languages. Users can use these
|
||||
// to create smaller collections of supported languages and reduce total table
|
||||
// size.
|
||||
|
||||
// The variable names defined here correspond to those in package language.
|
||||
|
||||
var (
|
||||
Afrikaans *Dictionary = &af // af
|
||||
Amharic *Dictionary = &am // am
|
||||
Arabic *Dictionary = &ar // ar
|
||||
ModernStandardArabic *Dictionary = Arabic // ar-001
|
||||
Azerbaijani *Dictionary = &az // az
|
||||
Bulgarian *Dictionary = &bg // bg
|
||||
Bengali *Dictionary = &bn // bn
|
||||
Catalan *Dictionary = &ca // ca
|
||||
Czech *Dictionary = &cs // cs
|
||||
Danish *Dictionary = &da // da
|
||||
German *Dictionary = &de // de
|
||||
Greek *Dictionary = &el // el
|
||||
English *Dictionary = &en // en
|
||||
AmericanEnglish *Dictionary = English // en-US
|
||||
BritishEnglish *Dictionary = English // en-GB
|
||||
Spanish *Dictionary = &es // es
|
||||
EuropeanSpanish *Dictionary = Spanish // es-ES
|
||||
LatinAmericanSpanish *Dictionary = Spanish // es-419
|
||||
Estonian *Dictionary = &et // et
|
||||
Persian *Dictionary = &fa // fa
|
||||
Finnish *Dictionary = &fi // fi
|
||||
Filipino *Dictionary = &fil // fil
|
||||
French *Dictionary = &fr // fr
|
||||
Gujarati *Dictionary = &gu // gu
|
||||
Hebrew *Dictionary = &he // he
|
||||
Hindi *Dictionary = &hi // hi
|
||||
Croatian *Dictionary = &hr // hr
|
||||
Hungarian *Dictionary = &hu // hu
|
||||
Armenian *Dictionary = &hy // hy
|
||||
Indonesian *Dictionary = &id // id
|
||||
Icelandic *Dictionary = &is // is
|
||||
Italian *Dictionary = &it // it
|
||||
Japanese *Dictionary = &ja // ja
|
||||
Georgian *Dictionary = &ka // ka
|
||||
Kazakh *Dictionary = &kk // kk
|
||||
Khmer *Dictionary = &km // km
|
||||
Kannada *Dictionary = &kn // kn
|
||||
Korean *Dictionary = &ko // ko
|
||||
Kirghiz *Dictionary = &ky // ky
|
||||
Lao *Dictionary = &lo // lo
|
||||
Lithuanian *Dictionary = < // lt
|
||||
Latvian *Dictionary = &lv // lv
|
||||
Macedonian *Dictionary = &mk // mk
|
||||
Malayalam *Dictionary = &ml // ml
|
||||
Mongolian *Dictionary = &mn // mn
|
||||
Marathi *Dictionary = &mr // mr
|
||||
Malay *Dictionary = &ms // ms
|
||||
Burmese *Dictionary = &my // my
|
||||
Nepali *Dictionary = &ne // ne
|
||||
Dutch *Dictionary = &nl // nl
|
||||
Norwegian *Dictionary = &no // no
|
||||
Punjabi *Dictionary = &pa // pa
|
||||
Polish *Dictionary = &pl // pl
|
||||
Portuguese *Dictionary = &pt // pt
|
||||
BrazilianPortuguese *Dictionary = Portuguese // pt-BR
|
||||
EuropeanPortuguese *Dictionary = &ptPT // pt-PT
|
||||
Romanian *Dictionary = &ro // ro
|
||||
Russian *Dictionary = &ru // ru
|
||||
Sinhala *Dictionary = &si // si
|
||||
Slovak *Dictionary = &sk // sk
|
||||
Slovenian *Dictionary = &sl // sl
|
||||
Albanian *Dictionary = &sq // sq
|
||||
Serbian *Dictionary = &sr // sr
|
||||
SerbianLatin *Dictionary = &srLatn // sr
|
||||
Swedish *Dictionary = &sv // sv
|
||||
Swahili *Dictionary = &sw // sw
|
||||
Tamil *Dictionary = &ta // ta
|
||||
Telugu *Dictionary = &te // te
|
||||
Thai *Dictionary = &th // th
|
||||
Turkish *Dictionary = &tr // tr
|
||||
Ukrainian *Dictionary = &uk // uk
|
||||
Urdu *Dictionary = &ur // ur
|
||||
Uzbek *Dictionary = &uz // uz
|
||||
Vietnamese *Dictionary = &vi // vi
|
||||
Chinese *Dictionary = &zh // zh
|
||||
SimplifiedChinese *Dictionary = Chinese // zh-Hans
|
||||
TraditionalChinese *Dictionary = &zhHant // zh-Hant
|
||||
Zulu *Dictionary = &zu // zu
|
||||
)
|
|
@ -0,0 +1,420 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:generate go run maketables.go -output tables.go
|
||||
|
||||
// Package display provides display names for languages, scripts and regions in
|
||||
// a requested language.
|
||||
//
|
||||
// The data is based on CLDR's localeDisplayNames. It includes the names of the
|
||||
// draft level "contributed" or "approved". The resulting tables are quite
|
||||
// large. The display package is designed so that users can reduce the linked-in
|
||||
// table sizes by cherry picking the languages one wishes to support. There is a
|
||||
// Dictionary defined for a selected set of common languages for this purpose.
|
||||
package display // import "golang.org/x/text/language/display"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/internal/format"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
/*
|
||||
TODO:
|
||||
All fairly low priority at the moment:
|
||||
- Include alternative and variants as an option (using func options).
|
||||
- Option for returning the empty string for undefined values.
|
||||
- Support variants, currencies, time zones, option names and other data
|
||||
provided in CLDR.
|
||||
- Do various optimizations:
|
||||
- Reduce size of offset tables.
|
||||
- Consider compressing infrequently used languages and decompress on demand.
|
||||
*/
|
||||
|
||||
// A Formatter formats a tag in the current language. It is used in conjunction
|
||||
// with the message package.
|
||||
type Formatter struct {
|
||||
lookup func(tag int, x interface{}) string
|
||||
x interface{}
|
||||
}
|
||||
|
||||
// Format implements "golang.org/x/text/internal/format".Formatter.
|
||||
func (f Formatter) Format(state format.State, verb rune) {
|
||||
// TODO: there are a lot of inefficiencies in this code. Fix it when we
|
||||
// language.Tag has embedded compact tags.
|
||||
t := state.Language()
|
||||
_, index, _ := matcher.Match(t)
|
||||
str := f.lookup(index, f.x)
|
||||
if str == "" {
|
||||
// TODO: use language-specific punctuation.
|
||||
// TODO: use codePattern instead of language?
|
||||
if unknown := f.lookup(index, language.Und); unknown != "" {
|
||||
fmt.Fprintf(state, "%v (%v)", unknown, f.x)
|
||||
} else {
|
||||
fmt.Fprintf(state, "[language: %v]", f.x)
|
||||
}
|
||||
} else {
|
||||
state.Write([]byte(str))
|
||||
}
|
||||
}
|
||||
|
||||
// Language returns a Formatter that renders the name for lang in the
|
||||
// current language. x may be a language.Base or a language.Tag.
|
||||
// It renders lang in the default language if no translation for the current
|
||||
// language is supported.
|
||||
func Language(lang interface{}) Formatter {
|
||||
return Formatter{langFunc, lang}
|
||||
}
|
||||
|
||||
// Region returns a Formatter that renders the name for region in the current
|
||||
// language. region may be a language.Region or a language.Tag.
|
||||
// It renders region in the default language if no translation for the current
|
||||
// language is supported.
|
||||
func Region(region interface{}) Formatter {
|
||||
return Formatter{regionFunc, region}
|
||||
}
|
||||
|
||||
// Script returns a Formatter that renders the name for script in the current
|
||||
// language. script may be a language.Script or a language.Tag.
|
||||
// It renders script in the default language if no translation for the current
|
||||
// language is supported.
|
||||
func Script(script interface{}) Formatter {
|
||||
return Formatter{scriptFunc, script}
|
||||
}
|
||||
|
||||
// Tag returns a Formatter that renders the name for tag in the current
|
||||
// language. tag may be a language.Tag.
|
||||
// It renders tag in the default language if no translation for the current
|
||||
// language is supported.
|
||||
func Tag(tag interface{}) Formatter {
|
||||
return Formatter{tagFunc, tag}
|
||||
}
|
||||
|
||||
// A Namer is used to get the name for a given value, such as a Tag, Language,
|
||||
// Script or Region.
|
||||
type Namer interface {
|
||||
// Name returns a display string for the given value. A Namer returns an
|
||||
// empty string for values it does not support. A Namer may support naming
|
||||
// an unspecified value. For example, when getting the name for a region for
|
||||
// a tag that does not have a defined Region, it may return the name for an
|
||||
// unknown region. It is up to the user to filter calls to Name for values
|
||||
// for which one does not want to have a name string.
|
||||
Name(x interface{}) string
|
||||
}
|
||||
|
||||
var (
|
||||
// Supported lists the languages for which names are defined.
|
||||
Supported language.Coverage
|
||||
|
||||
// The set of all possible values for which names are defined. Note that not
|
||||
// all Namer implementations will cover all the values of a given type.
|
||||
// A Namer will return the empty string for unsupported values.
|
||||
Values language.Coverage
|
||||
|
||||
matcher language.Matcher
|
||||
)
|
||||
|
||||
func init() {
|
||||
tags := make([]language.Tag, numSupported)
|
||||
s := supported
|
||||
for i := range tags {
|
||||
p := strings.IndexByte(s, '|')
|
||||
tags[i] = language.Raw.Make(s[:p])
|
||||
s = s[p+1:]
|
||||
}
|
||||
matcher = language.NewMatcher(tags)
|
||||
Supported = language.NewCoverage(tags)
|
||||
|
||||
Values = language.NewCoverage(langTagSet.Tags, supportedScripts, supportedRegions)
|
||||
}
|
||||
|
||||
// Languages returns a Namer for naming languages. It returns nil if there is no
|
||||
// data for the given tag. The type passed to Name must be either language.Base
|
||||
// or language.Tag. Note that the result may differ between passing a tag or its
|
||||
// base language. For example, for English, passing "nl-BE" would return Flemish
|
||||
// whereas passing "nl" returns "Dutch".
|
||||
func Languages(t language.Tag) Namer {
|
||||
if _, index, conf := matcher.Match(t); conf != language.No {
|
||||
return languageNamer(index)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type languageNamer int
|
||||
|
||||
func langFunc(i int, x interface{}) string {
|
||||
return nameLanguage(languageNamer(i), x)
|
||||
}
|
||||
|
||||
func (n languageNamer) name(i int) string {
|
||||
return lookup(langHeaders[:], int(n), i)
|
||||
}
|
||||
|
||||
// Name implements the Namer interface for language names.
|
||||
func (n languageNamer) Name(x interface{}) string {
|
||||
return nameLanguage(n, x)
|
||||
}
|
||||
|
||||
// nonEmptyIndex walks up the parent chain until a non-empty header is found.
|
||||
// It returns -1 if no index could be found.
|
||||
func nonEmptyIndex(h []header, index int) int {
|
||||
for ; index != -1 && h[index].data == ""; index = int(parents[index]) {
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// Scripts returns a Namer for naming scripts. It returns nil if there is no
|
||||
// data for the given tag. The type passed to Name must be either a
|
||||
// language.Script or a language.Tag. It will not attempt to infer a script for
|
||||
// tags with an unspecified script.
|
||||
func Scripts(t language.Tag) Namer {
|
||||
if _, index, conf := matcher.Match(t); conf != language.No {
|
||||
if index = nonEmptyIndex(scriptHeaders[:], index); index != -1 {
|
||||
return scriptNamer(index)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type scriptNamer int
|
||||
|
||||
func scriptFunc(i int, x interface{}) string {
|
||||
return nameScript(scriptNamer(i), x)
|
||||
}
|
||||
|
||||
func (n scriptNamer) name(i int) string {
|
||||
return lookup(scriptHeaders[:], int(n), i)
|
||||
}
|
||||
|
||||
// Name implements the Namer interface for script names.
|
||||
func (n scriptNamer) Name(x interface{}) string {
|
||||
return nameScript(n, x)
|
||||
}
|
||||
|
||||
// Regions returns a Namer for naming regions. It returns nil if there is no
|
||||
// data for the given tag. The type passed to Name must be either a
|
||||
// language.Region or a language.Tag. It will not attempt to infer a region for
|
||||
// tags with an unspecified region.
|
||||
func Regions(t language.Tag) Namer {
|
||||
if _, index, conf := matcher.Match(t); conf != language.No {
|
||||
if index = nonEmptyIndex(regionHeaders[:], index); index != -1 {
|
||||
return regionNamer(index)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type regionNamer int
|
||||
|
||||
func regionFunc(i int, x interface{}) string {
|
||||
return nameRegion(regionNamer(i), x)
|
||||
}
|
||||
|
||||
func (n regionNamer) name(i int) string {
|
||||
return lookup(regionHeaders[:], int(n), i)
|
||||
}
|
||||
|
||||
// Name implements the Namer interface for region names.
|
||||
func (n regionNamer) Name(x interface{}) string {
|
||||
return nameRegion(n, x)
|
||||
}
|
||||
|
||||
// Tags returns a Namer for giving a full description of a tag. The names of
|
||||
// scripts and regions that are not already implied by the language name will
|
||||
// in appended within parentheses. It returns nil if there is not data for the
|
||||
// given tag. The type passed to Name must be a tag.
|
||||
func Tags(t language.Tag) Namer {
|
||||
if _, index, conf := matcher.Match(t); conf != language.No {
|
||||
return tagNamer(index)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type tagNamer int
|
||||
|
||||
func tagFunc(i int, x interface{}) string {
|
||||
return nameTag(languageNamer(i), scriptNamer(i), regionNamer(i), x)
|
||||
}
|
||||
|
||||
// Name implements the Namer interface for tag names.
|
||||
func (n tagNamer) Name(x interface{}) string {
|
||||
return nameTag(languageNamer(n), scriptNamer(n), regionNamer(n), x)
|
||||
}
|
||||
|
||||
// lookup finds the name for an entry in a global table, traversing the
|
||||
// inheritance hierarchy if needed.
|
||||
func lookup(table []header, dict, want int) string {
|
||||
for dict != -1 {
|
||||
if s := table[dict].name(want); s != "" {
|
||||
return s
|
||||
}
|
||||
dict = int(parents[dict])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A Dictionary holds a collection of Namers for a single language. One can
|
||||
// reduce the amount of data linked in to a binary by only referencing
|
||||
// Dictionaries for the languages one needs to support instead of using the
|
||||
// generic Namer factories.
|
||||
type Dictionary struct {
|
||||
parent *Dictionary
|
||||
lang header
|
||||
script header
|
||||
region header
|
||||
}
|
||||
|
||||
// Tags returns a Namer for giving a full description of a tag. The names of
|
||||
// scripts and regions that are not already implied by the language name will
|
||||
// in appended within parentheses. It returns nil if there is not data for the
|
||||
// given tag. The type passed to Name must be a tag.
|
||||
func (d *Dictionary) Tags() Namer {
|
||||
return dictTags{d}
|
||||
}
|
||||
|
||||
type dictTags struct {
|
||||
d *Dictionary
|
||||
}
|
||||
|
||||
// Name implements the Namer interface for tag names.
|
||||
func (n dictTags) Name(x interface{}) string {
|
||||
return nameTag(dictLanguages{n.d}, dictScripts{n.d}, dictRegions{n.d}, x)
|
||||
}
|
||||
|
||||
// Languages returns a Namer for naming languages. It returns nil if there is no
|
||||
// data for the given tag. The type passed to Name must be either language.Base
|
||||
// or language.Tag. Note that the result may differ between passing a tag or its
|
||||
// base language. For example, for English, passing "nl-BE" would return Flemish
|
||||
// whereas passing "nl" returns "Dutch".
|
||||
func (d *Dictionary) Languages() Namer {
|
||||
return dictLanguages{d}
|
||||
}
|
||||
|
||||
type dictLanguages struct {
|
||||
d *Dictionary
|
||||
}
|
||||
|
||||
func (n dictLanguages) name(i int) string {
|
||||
for d := n.d; d != nil; d = d.parent {
|
||||
if s := d.lang.name(i); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Name implements the Namer interface for language names.
|
||||
func (n dictLanguages) Name(x interface{}) string {
|
||||
return nameLanguage(n, x)
|
||||
}
|
||||
|
||||
// Scripts returns a Namer for naming scripts. It returns nil if there is no
|
||||
// data for the given tag. The type passed to Name must be either a
|
||||
// language.Script or a language.Tag. It will not attempt to infer a script for
|
||||
// tags with an unspecified script.
|
||||
func (d *Dictionary) Scripts() Namer {
|
||||
return dictScripts{d}
|
||||
}
|
||||
|
||||
type dictScripts struct {
|
||||
d *Dictionary
|
||||
}
|
||||
|
||||
func (n dictScripts) name(i int) string {
|
||||
for d := n.d; d != nil; d = d.parent {
|
||||
if s := d.script.name(i); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Name implements the Namer interface for script names.
|
||||
func (n dictScripts) Name(x interface{}) string {
|
||||
return nameScript(n, x)
|
||||
}
|
||||
|
||||
// Regions returns a Namer for naming regions. It returns nil if there is no
|
||||
// data for the given tag. The type passed to Name must be either a
|
||||
// language.Region or a language.Tag. It will not attempt to infer a region for
|
||||
// tags with an unspecified region.
|
||||
func (d *Dictionary) Regions() Namer {
|
||||
return dictRegions{d}
|
||||
}
|
||||
|
||||
type dictRegions struct {
|
||||
d *Dictionary
|
||||
}
|
||||
|
||||
func (n dictRegions) name(i int) string {
|
||||
for d := n.d; d != nil; d = d.parent {
|
||||
if s := d.region.name(i); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Name implements the Namer interface for region names.
|
||||
func (n dictRegions) Name(x interface{}) string {
|
||||
return nameRegion(n, x)
|
||||
}
|
||||
|
||||
// A SelfNamer implements a Namer that returns the name of language in this same
|
||||
// language. It provides a very compact mechanism to provide a comprehensive
|
||||
// list of languages to users in their native language.
|
||||
type SelfNamer struct {
|
||||
// Supported defines the values supported by this Namer.
|
||||
Supported language.Coverage
|
||||
}
|
||||
|
||||
var (
|
||||
// Self is a shared instance of a SelfNamer.
|
||||
Self *SelfNamer = &self
|
||||
|
||||
self = SelfNamer{language.NewCoverage(selfTagSet.Tags)}
|
||||
)
|
||||
|
||||
// Name returns the name of a given language tag in the language identified by
|
||||
// this tag. It supports both the language.Base and language.Tag types.
|
||||
func (n SelfNamer) Name(x interface{}) string {
|
||||
t, _ := language.All.Compose(x)
|
||||
base, scr, reg := t.Raw()
|
||||
baseScript := language.Script{}
|
||||
if (scr == language.Script{} && reg != language.Region{}) {
|
||||
// For looking up in the self dictionary, we need to select the
|
||||
// maximized script. This is even the case if the script isn't
|
||||
// specified.
|
||||
s1, _ := t.Script()
|
||||
if baseScript = getScript(base); baseScript != s1 {
|
||||
scr = s1
|
||||
}
|
||||
}
|
||||
|
||||
i, scr, reg := selfTagSet.index(base, scr, reg)
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Only return the display name if the script matches the expected script.
|
||||
if (scr != language.Script{}) {
|
||||
if (baseScript == language.Script{}) {
|
||||
baseScript = getScript(base)
|
||||
}
|
||||
if baseScript != scr {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
return selfHeaders[0].name(i)
|
||||
}
|
||||
|
||||
// getScript returns the maximized script for a base language.
|
||||
func getScript(b language.Base) language.Script {
|
||||
tag, _ := language.Raw.Compose(b)
|
||||
scr, _ := tag.Script()
|
||||
return scr
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package display
|
||||
|
||||
// This file contains common lookup code that is shared between the various
|
||||
// implementations of Namer and Dictionaries.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type namer interface {
|
||||
// name gets the string for the given index. It should walk the
|
||||
// inheritance chain if a value is not present in the base index.
|
||||
name(idx int) string
|
||||
}
|
||||
|
||||
func nameLanguage(n namer, x interface{}) string {
|
||||
t, _ := language.All.Compose(x)
|
||||
for {
|
||||
i, _, _ := langTagSet.index(t.Raw())
|
||||
if s := n.name(i); s != "" {
|
||||
return s
|
||||
}
|
||||
if t = t.Parent(); t == language.Und {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nameScript(n namer, x interface{}) string {
|
||||
t, _ := language.DeprecatedScript.Compose(x)
|
||||
_, s, _ := t.Raw()
|
||||
return n.name(scriptIndex.index(s.String()))
|
||||
}
|
||||
|
||||
func nameRegion(n namer, x interface{}) string {
|
||||
t, _ := language.DeprecatedRegion.Compose(x)
|
||||
_, _, r := t.Raw()
|
||||
return n.name(regionIndex.index(r.String()))
|
||||
}
|
||||
|
||||
func nameTag(langN, scrN, regN namer, x interface{}) string {
|
||||
t, ok := x.(language.Tag)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
const form = language.All &^ language.SuppressScript
|
||||
if c, err := form.Canonicalize(t); err == nil {
|
||||
t = c
|
||||
}
|
||||
_, sRaw, rRaw := t.Raw()
|
||||
i, scr, reg := langTagSet.index(t.Raw())
|
||||
for i != -1 {
|
||||
if str := langN.name(i); str != "" {
|
||||
if hasS, hasR := (scr != language.Script{}), (reg != language.Region{}); hasS || hasR {
|
||||
ss, sr := "", ""
|
||||
if hasS {
|
||||
ss = scrN.name(scriptIndex.index(scr.String()))
|
||||
}
|
||||
if hasR {
|
||||
sr = regN.name(regionIndex.index(reg.String()))
|
||||
}
|
||||
// TODO: use patterns in CLDR or at least confirm they are the
|
||||
// same for all languages.
|
||||
if ss != "" && sr != "" {
|
||||
return fmt.Sprintf("%s (%s, %s)", str, ss, sr)
|
||||
}
|
||||
if ss != "" || sr != "" {
|
||||
return fmt.Sprintf("%s (%s%s)", str, ss, sr)
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
scr, reg = sRaw, rRaw
|
||||
if t = t.Parent(); t == language.Und {
|
||||
return ""
|
||||
}
|
||||
i, _, _ = langTagSet.index(t.Raw())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// header contains the data and indexes for a single namer.
|
||||
// data contains a series of strings concatenated into one. index contains the
|
||||
// offsets for a string in data. For example, consider a header that defines
|
||||
// strings for the languages de, el, en, fi, and nl:
|
||||
//
|
||||
// header{
|
||||
// data: "GermanGreekEnglishDutch",
|
||||
// index: []uint16{0, 6, 11, 18, 18, 23},
|
||||
// }
|
||||
//
|
||||
// For a language with index i, the string is defined by
|
||||
// data[index[i]:index[i+1]]. So the number of elements in index is always one
|
||||
// greater than the number of languages for which header defines a value.
|
||||
// A string for a language may be empty, which means the name is undefined. In
|
||||
// the above example, the name for fi (Finnish) is undefined.
|
||||
type header struct {
|
||||
data string
|
||||
index []uint16
|
||||
}
|
||||
|
||||
// name looks up the name for a tag in the dictionary, given its index.
|
||||
func (h *header) name(i int) string {
|
||||
if 0 <= i && i < len(h.index)-1 {
|
||||
return h.data[h.index[i]:h.index[i+1]]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// tagSet is used to find the index of a language in a set of tags.
|
||||
type tagSet struct {
|
||||
single tagIndex
|
||||
long []string
|
||||
}
|
||||
|
||||
var (
|
||||
langTagSet = tagSet{
|
||||
single: langIndex,
|
||||
long: langTagsLong,
|
||||
}
|
||||
|
||||
// selfTagSet is used for indexing the language strings in their own
|
||||
// language.
|
||||
selfTagSet = tagSet{
|
||||
single: selfIndex,
|
||||
long: selfTagsLong,
|
||||
}
|
||||
|
||||
zzzz = language.MustParseScript("Zzzz")
|
||||
zz = language.MustParseRegion("ZZ")
|
||||
)
|
||||
|
||||
// index returns the index of the tag for the given base, script and region or
|
||||
// its parent if the tag is not available. If the match is for a parent entry,
|
||||
// the excess script and region are returned.
|
||||
func (ts *tagSet) index(base language.Base, scr language.Script, reg language.Region) (int, language.Script, language.Region) {
|
||||
lang := base.String()
|
||||
index := -1
|
||||
if (scr != language.Script{} || reg != language.Region{}) {
|
||||
if scr == zzzz {
|
||||
scr = language.Script{}
|
||||
}
|
||||
if reg == zz {
|
||||
reg = language.Region{}
|
||||
}
|
||||
|
||||
i := sort.SearchStrings(ts.long, lang)
|
||||
// All entries have either a script or a region and not both.
|
||||
scrStr, regStr := scr.String(), reg.String()
|
||||
for ; i < len(ts.long) && strings.HasPrefix(ts.long[i], lang); i++ {
|
||||
if s := ts.long[i][len(lang)+1:]; s == scrStr {
|
||||
scr = language.Script{}
|
||||
index = i + ts.single.len()
|
||||
break
|
||||
} else if s == regStr {
|
||||
reg = language.Region{}
|
||||
index = i + ts.single.len()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
index = ts.single.index(lang)
|
||||
}
|
||||
return index, scr, reg
|
||||
}
|
||||
|
||||
func (ts *tagSet) Tags() []language.Tag {
|
||||
tags := make([]language.Tag, 0, ts.single.len()+len(ts.long))
|
||||
ts.single.keys(func(s string) {
|
||||
tags = append(tags, language.Raw.MustParse(s))
|
||||
})
|
||||
for _, s := range ts.long {
|
||||
tags = append(tags, language.Raw.MustParse(s))
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func supportedScripts() []language.Script {
|
||||
scr := make([]language.Script, 0, scriptIndex.len())
|
||||
scriptIndex.keys(func(s string) {
|
||||
scr = append(scr, language.MustParseScript(s))
|
||||
})
|
||||
return scr
|
||||
}
|
||||
|
||||
func supportedRegions() []language.Region {
|
||||
reg := make([]language.Region, 0, regionIndex.len())
|
||||
regionIndex.keys(func(s string) {
|
||||
reg = append(reg, language.MustParseRegion(s))
|
||||
})
|
||||
return reg
|
||||
}
|
||||
|
||||
// tagIndex holds a concatenated lists of subtags of length 2 to 4, one string
|
||||
// for each length, which can be used in combination with binary search to get
|
||||
// the index associated with a tag.
|
||||
// For example, a tagIndex{
|
||||
//
|
||||
// "arenesfrruzh", // 6 2-byte tags.
|
||||
// "barwae", // 2 3-byte tags.
|
||||
// "",
|
||||
//
|
||||
// }
|
||||
// would mean that the 2-byte tag "fr" had an index of 3, and the 3-byte tag
|
||||
// "wae" had an index of 7.
|
||||
type tagIndex [3]string
|
||||
|
||||
func (t *tagIndex) index(s string) int {
|
||||
sz := len(s)
|
||||
if sz < 2 || 4 < sz {
|
||||
return -1
|
||||
}
|
||||
a := t[sz-2]
|
||||
index := sort.Search(len(a)/sz, func(i int) bool {
|
||||
p := i * sz
|
||||
return a[p:p+sz] >= s
|
||||
})
|
||||
p := index * sz
|
||||
if end := p + sz; end > len(a) || a[p:end] != s {
|
||||
return -1
|
||||
}
|
||||
// Add the number of tags for smaller sizes.
|
||||
for i := 0; i < sz-2; i++ {
|
||||
index += len(t[i]) / (i + 2)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// len returns the number of tags that are contained in the tagIndex.
|
||||
func (t *tagIndex) len() (n int) {
|
||||
for i, s := range t {
|
||||
n += len(s) / (i + 2)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// keys calls f for each tag.
|
||||
func (t *tagIndex) keys(f func(key string)) {
|
||||
for i, s := range *t {
|
||||
for ; s != ""; s = s[i+2:] {
|
||||
f(s[:i+2])
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -55,6 +55,7 @@ golang.org/x/text/internal/number
|
|||
golang.org/x/text/internal/stringset
|
||||
golang.org/x/text/internal/tag
|
||||
golang.org/x/text/language
|
||||
golang.org/x/text/language/display
|
||||
golang.org/x/text/message
|
||||
golang.org/x/text/message/catalog
|
||||
# gopkg.in/yaml.v2 v2.3.0
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
T(format message.Reference, v ...any)
|
||||
Title()
|
||||
Lang()
|
||||
Dir()
|
||||
} %}
|
||||
|
||||
{% code
|
||||
|
@ -21,7 +22,7 @@ type BaseOf struct {
|
|||
func NewBaseOf(site *domain.Site) BaseOf {
|
||||
return BaseOf{
|
||||
site: site,
|
||||
printer: message.NewPrinter(site.Language),
|
||||
printer: message.NewPrinter(language.Make(site.Language.Code())),
|
||||
}
|
||||
}
|
||||
%}
|
||||
|
@ -38,14 +39,16 @@ func NewBaseOf(site *domain.Site) BaseOf {
|
|||
{% func (b BaseOf) Body() %}{% endfunc %}
|
||||
|
||||
{% func (b BaseOf) Lang() %}
|
||||
{% if b.site.Language != language.Und %}
|
||||
{%s b.site.Language.String() %}
|
||||
{% endif %}
|
||||
{%s b.site.Language.Lang() %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func (b BaseOf) Dir() %}
|
||||
{%s b.site.Language.Dir() %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func Template(p Pager) %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{%= p.Lang() %}">
|
||||
<html lang="{%= p.Lang() %}" dir="{%= p.Dir() %}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
|
|
@ -51,10 +51,16 @@ type Pager interface {
|
|||
StreamLang(qw422016 *qt422016.Writer)
|
||||
//line web/template/baseof.qtpl:8
|
||||
WriteLang(qq422016 qtio422016.Writer)
|
||||
//line web/template/baseof.qtpl:8
|
||||
Dir() string
|
||||
//line web/template/baseof.qtpl:8
|
||||
StreamDir(qw422016 *qt422016.Writer)
|
||||
//line web/template/baseof.qtpl:8
|
||||
WriteDir(qq422016 qtio422016.Writer)
|
||||
//line web/template/baseof.qtpl:8
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:16
|
||||
//line web/template/baseof.qtpl:17
|
||||
type BaseOf struct {
|
||||
printer *message.Printer
|
||||
site *domain.Site
|
||||
|
@ -63,185 +69,218 @@ type BaseOf struct {
|
|||
func NewBaseOf(site *domain.Site) BaseOf {
|
||||
return BaseOf{
|
||||
site: site,
|
||||
printer: message.NewPrinter(site.Language),
|
||||
printer: message.NewPrinter(language.Make(site.Language.Code())),
|
||||
}
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:30
|
||||
func (b BaseOf) StreamTitle(qw422016 *qt422016.Writer) {
|
||||
//line web/template/baseof.qtpl:31
|
||||
func (b BaseOf) StreamTitle(qw422016 *qt422016.Writer) {
|
||||
//line web/template/baseof.qtpl:32
|
||||
qw422016.E().S(b.site.Title)
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
func (b BaseOf) WriteTitle(qq422016 qtio422016.Writer) {
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
b.StreamTitle(qw422016)
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
func (b BaseOf) Title() string {
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
b.WriteTitle(qb422016)
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
return qs422016
|
||||
//line web/template/baseof.qtpl:32
|
||||
//line web/template/baseof.qtpl:33
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:34
|
||||
func (b BaseOf) StreamT(qw422016 *qt422016.Writer, key message.Reference, a ...any) {
|
||||
//line web/template/baseof.qtpl:35
|
||||
func (b BaseOf) StreamT(qw422016 *qt422016.Writer, key message.Reference, a ...any) {
|
||||
//line web/template/baseof.qtpl:36
|
||||
qw422016.E().S(b.printer.Sprintf(key, a...))
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
func (b BaseOf) WriteT(qq422016 qtio422016.Writer, key message.Reference, a ...any) {
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
b.StreamT(qw422016, key, a...)
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
func (b BaseOf) T(key message.Reference, a ...any) string {
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
b.WriteT(qb422016, key, a...)
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
return qs422016
|
||||
//line web/template/baseof.qtpl:36
|
||||
//line web/template/baseof.qtpl:37
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
func (b BaseOf) StreamBody(qw422016 *qt422016.Writer) {
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
func (b BaseOf) WriteBody(qq422016 qtio422016.Writer) {
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
b.StreamBody(qw422016)
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
func (b BaseOf) Body() string {
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
b.WriteBody(qb422016)
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
return qs422016
|
||||
//line web/template/baseof.qtpl:38
|
||||
//line web/template/baseof.qtpl:39
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:40
|
||||
func (b BaseOf) StreamLang(qw422016 *qt422016.Writer) {
|
||||
//line web/template/baseof.qtpl:41
|
||||
if b.site.Language != language.Und {
|
||||
func (b BaseOf) StreamLang(qw422016 *qt422016.Writer) {
|
||||
//line web/template/baseof.qtpl:42
|
||||
qw422016.E().S(b.site.Language.String())
|
||||
qw422016.E().S(b.site.Language.Lang())
|
||||
//line web/template/baseof.qtpl:43
|
||||
}
|
||||
//line web/template/baseof.qtpl:44
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
func (b BaseOf) WriteLang(qq422016 qtio422016.Writer) {
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
b.StreamLang(qw422016)
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
func (b BaseOf) Lang() string {
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
b.WriteLang(qb422016)
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
return qs422016
|
||||
//line web/template/baseof.qtpl:44
|
||||
//line web/template/baseof.qtpl:43
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:45
|
||||
func (b BaseOf) StreamDir(qw422016 *qt422016.Writer) {
|
||||
//line web/template/baseof.qtpl:46
|
||||
func StreamTemplate(qw422016 *qt422016.Writer, p Pager) {
|
||||
//line web/template/baseof.qtpl:46
|
||||
qw422016.N().S(`<!DOCTYPE html><html lang="`)
|
||||
//line web/template/baseof.qtpl:48
|
||||
p.StreamLang(qw422016)
|
||||
//line web/template/baseof.qtpl:48
|
||||
qw422016.N().S(`"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>`)
|
||||
//line web/template/baseof.qtpl:53
|
||||
p.StreamTitle(qw422016)
|
||||
//line web/template/baseof.qtpl:53
|
||||
qw422016.N().S(`</title></head><body>`)
|
||||
//line web/template/baseof.qtpl:57
|
||||
p.StreamBody(qw422016)
|
||||
//line web/template/baseof.qtpl:57
|
||||
qw422016.N().S(`</body></html>`)
|
||||
//line web/template/baseof.qtpl:60
|
||||
qw422016.E().S(b.site.Language.Dir())
|
||||
//line web/template/baseof.qtpl:47
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:60
|
||||
func WriteTemplate(qq422016 qtio422016.Writer, p Pager) {
|
||||
//line web/template/baseof.qtpl:60
|
||||
//line web/template/baseof.qtpl:47
|
||||
func (b BaseOf) WriteDir(qq422016 qtio422016.Writer) {
|
||||
//line web/template/baseof.qtpl:47
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/baseof.qtpl:60
|
||||
StreamTemplate(qw422016, p)
|
||||
//line web/template/baseof.qtpl:60
|
||||
//line web/template/baseof.qtpl:47
|
||||
b.StreamDir(qw422016)
|
||||
//line web/template/baseof.qtpl:47
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/baseof.qtpl:60
|
||||
//line web/template/baseof.qtpl:47
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:60
|
||||
func Template(p Pager) string {
|
||||
//line web/template/baseof.qtpl:60
|
||||
//line web/template/baseof.qtpl:47
|
||||
func (b BaseOf) Dir() string {
|
||||
//line web/template/baseof.qtpl:47
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/baseof.qtpl:60
|
||||
WriteTemplate(qb422016, p)
|
||||
//line web/template/baseof.qtpl:60
|
||||
//line web/template/baseof.qtpl:47
|
||||
b.WriteDir(qb422016)
|
||||
//line web/template/baseof.qtpl:47
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/baseof.qtpl:60
|
||||
//line web/template/baseof.qtpl:47
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/baseof.qtpl:60
|
||||
//line web/template/baseof.qtpl:47
|
||||
return qs422016
|
||||
//line web/template/baseof.qtpl:60
|
||||
//line web/template/baseof.qtpl:47
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:49
|
||||
func StreamTemplate(qw422016 *qt422016.Writer, p Pager) {
|
||||
//line web/template/baseof.qtpl:49
|
||||
qw422016.N().S(`<!DOCTYPE html><html lang="`)
|
||||
//line web/template/baseof.qtpl:51
|
||||
p.StreamLang(qw422016)
|
||||
//line web/template/baseof.qtpl:51
|
||||
qw422016.N().S(`" dir="`)
|
||||
//line web/template/baseof.qtpl:51
|
||||
p.StreamDir(qw422016)
|
||||
//line web/template/baseof.qtpl:51
|
||||
qw422016.N().S(`"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>`)
|
||||
//line web/template/baseof.qtpl:56
|
||||
p.StreamTitle(qw422016)
|
||||
//line web/template/baseof.qtpl:56
|
||||
qw422016.N().S(`</title></head><body>`)
|
||||
//line web/template/baseof.qtpl:60
|
||||
p.StreamBody(qw422016)
|
||||
//line web/template/baseof.qtpl:60
|
||||
qw422016.N().S(`</body></html>`)
|
||||
//line web/template/baseof.qtpl:63
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:63
|
||||
func WriteTemplate(qq422016 qtio422016.Writer, p Pager) {
|
||||
//line web/template/baseof.qtpl:63
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/baseof.qtpl:63
|
||||
StreamTemplate(qw422016, p)
|
||||
//line web/template/baseof.qtpl:63
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/baseof.qtpl:63
|
||||
}
|
||||
|
||||
//line web/template/baseof.qtpl:63
|
||||
func Template(p Pager) string {
|
||||
//line web/template/baseof.qtpl:63
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/baseof.qtpl:63
|
||||
WriteTemplate(qb422016, p)
|
||||
//line web/template/baseof.qtpl:63
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/baseof.qtpl:63
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/baseof.qtpl:63
|
||||
return qs422016
|
||||
//line web/template/baseof.qtpl:63
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
{% import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
) %}
|
||||
|
||||
|
@ -28,8 +26,16 @@ func NewPage(base BaseOf, page *domain.Page) Page {
|
|||
{% endfunc %}
|
||||
|
||||
{% func (p Page) Lang() %}
|
||||
{% if p.page.Language != language.Und %}
|
||||
{%s p.page.Language.String() %}
|
||||
{% if p.page.Language != domain.LanguageUnd %}
|
||||
{%s p.page.Language.Lang() %}
|
||||
{% else %}
|
||||
{%= p.BaseOf.Lang() %}
|
||||
{% endif %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func (p Page) Dir() %}
|
||||
{% if p.page.Language != domain.LanguageUnd %}
|
||||
{%s p.page.Language.Dir() %}
|
||||
{% else %}
|
||||
{%= p.BaseOf.Lang() %}
|
||||
{% endif %}
|
||||
|
|
|
@ -6,25 +6,23 @@ package template
|
|||
|
||||
//line web/template/page.qtpl:1
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
//line web/template/page.qtpl:7
|
||||
//line web/template/page.qtpl:5
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line web/template/page.qtpl:7
|
||||
//line web/template/page.qtpl:5
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line web/template/page.qtpl:8
|
||||
//line web/template/page.qtpl:6
|
||||
type Page struct {
|
||||
BaseOf
|
||||
page *domain.Page
|
||||
|
@ -37,121 +35,162 @@ func NewPage(base BaseOf, page *domain.Page) Page {
|
|||
}
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:22
|
||||
//line web/template/page.qtpl:20
|
||||
func (p Page) StreamTitle(qw422016 *qt422016.Writer) {
|
||||
//line web/template/page.qtpl:23
|
||||
//line web/template/page.qtpl:21
|
||||
if p.page.Title != "" {
|
||||
//line web/template/page.qtpl:24
|
||||
//line web/template/page.qtpl:22
|
||||
qw422016.E().S(p.page.Title)
|
||||
//line web/template/page.qtpl:25
|
||||
//line web/template/page.qtpl:23
|
||||
} else {
|
||||
//line web/template/page.qtpl:26
|
||||
//line web/template/page.qtpl:24
|
||||
p.BaseOf.StreamTitle(qw422016)
|
||||
//line web/template/page.qtpl:27
|
||||
//line web/template/page.qtpl:25
|
||||
}
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
func (p Page) WriteTitle(qq422016 qtio422016.Writer) {
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
p.StreamTitle(qw422016)
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
func (p Page) Title() string {
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
p.WriteTitle(qb422016)
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
return qs422016
|
||||
//line web/template/page.qtpl:28
|
||||
//line web/template/page.qtpl:26
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:30
|
||||
//line web/template/page.qtpl:28
|
||||
func (p Page) StreamLang(qw422016 *qt422016.Writer) {
|
||||
//line web/template/page.qtpl:29
|
||||
if p.page.Language != domain.LanguageUnd {
|
||||
//line web/template/page.qtpl:30
|
||||
qw422016.E().S(p.page.Language.Lang())
|
||||
//line web/template/page.qtpl:31
|
||||
if p.page.Language != language.Und {
|
||||
//line web/template/page.qtpl:32
|
||||
qw422016.E().S(p.page.Language.String())
|
||||
//line web/template/page.qtpl:33
|
||||
} else {
|
||||
//line web/template/page.qtpl:34
|
||||
//line web/template/page.qtpl:32
|
||||
p.BaseOf.StreamLang(qw422016)
|
||||
//line web/template/page.qtpl:35
|
||||
//line web/template/page.qtpl:33
|
||||
}
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
func (p Page) WriteLang(qq422016 qtio422016.Writer) {
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
p.StreamLang(qw422016)
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
func (p Page) Lang() string {
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
p.WriteLang(qb422016)
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/page.qtpl:36
|
||||
//line web/template/page.qtpl:34
|
||||
return qs422016
|
||||
//line web/template/page.qtpl:34
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:36
|
||||
func (p Page) StreamDir(qw422016 *qt422016.Writer) {
|
||||
//line web/template/page.qtpl:37
|
||||
if p.page.Language != domain.LanguageUnd {
|
||||
//line web/template/page.qtpl:38
|
||||
qw422016.E().S(p.page.Language.Dir())
|
||||
//line web/template/page.qtpl:39
|
||||
} else {
|
||||
//line web/template/page.qtpl:40
|
||||
p.BaseOf.StreamLang(qw422016)
|
||||
//line web/template/page.qtpl:41
|
||||
}
|
||||
//line web/template/page.qtpl:42
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:38
|
||||
func (p Page) StreamBody(qw422016 *qt422016.Writer) {
|
||||
//line web/template/page.qtpl:38
|
||||
qw422016.N().S(`<p>`)
|
||||
//line web/template/page.qtpl:39
|
||||
qw422016.N().Z(p.page.Content)
|
||||
//line web/template/page.qtpl:39
|
||||
qw422016.N().S(`</p>`)
|
||||
//line web/template/page.qtpl:40
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:40
|
||||
func (p Page) WriteBody(qq422016 qtio422016.Writer) {
|
||||
//line web/template/page.qtpl:40
|
||||
//line web/template/page.qtpl:42
|
||||
func (p Page) WriteDir(qq422016 qtio422016.Writer) {
|
||||
//line web/template/page.qtpl:42
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/page.qtpl:40
|
||||
p.StreamBody(qw422016)
|
||||
//line web/template/page.qtpl:40
|
||||
//line web/template/page.qtpl:42
|
||||
p.StreamDir(qw422016)
|
||||
//line web/template/page.qtpl:42
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/page.qtpl:40
|
||||
//line web/template/page.qtpl:42
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:40
|
||||
func (p Page) Body() string {
|
||||
//line web/template/page.qtpl:40
|
||||
//line web/template/page.qtpl:42
|
||||
func (p Page) Dir() string {
|
||||
//line web/template/page.qtpl:42
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/page.qtpl:40
|
||||
p.WriteBody(qb422016)
|
||||
//line web/template/page.qtpl:40
|
||||
//line web/template/page.qtpl:42
|
||||
p.WriteDir(qb422016)
|
||||
//line web/template/page.qtpl:42
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/page.qtpl:40
|
||||
//line web/template/page.qtpl:42
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/page.qtpl:40
|
||||
//line web/template/page.qtpl:42
|
||||
return qs422016
|
||||
//line web/template/page.qtpl:40
|
||||
//line web/template/page.qtpl:42
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:44
|
||||
func (p Page) StreamBody(qw422016 *qt422016.Writer) {
|
||||
//line web/template/page.qtpl:44
|
||||
qw422016.N().S(`<p>`)
|
||||
//line web/template/page.qtpl:45
|
||||
qw422016.N().Z(p.page.Content)
|
||||
//line web/template/page.qtpl:45
|
||||
qw422016.N().S(`</p>`)
|
||||
//line web/template/page.qtpl:46
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:46
|
||||
func (p Page) WriteBody(qq422016 qtio422016.Writer) {
|
||||
//line web/template/page.qtpl:46
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/page.qtpl:46
|
||||
p.StreamBody(qw422016)
|
||||
//line web/template/page.qtpl:46
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/page.qtpl:46
|
||||
}
|
||||
|
||||
//line web/template/page.qtpl:46
|
||||
func (p Page) Body() string {
|
||||
//line web/template/page.qtpl:46
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/page.qtpl:46
|
||||
p.WriteBody(qb422016)
|
||||
//line web/template/page.qtpl:46
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/page.qtpl:46
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/page.qtpl:46
|
||||
return qs422016
|
||||
//line web/template/page.qtpl:46
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue