Bags fixed
This commit is contained in:
parent
ecb485fa98
commit
f375bbb4c6
42 changed files with 12813 additions and 38 deletions
13
README.en.md
13
README.en.md
|
|
@ -116,7 +116,14 @@ wayfarer_*.go, — Wayfarer: text-based space trading (galaxy/trading/
|
||||||
translations_wayfarer*.go combat/quests), inspired by Elite but not a copy of
|
translations_wayfarer*.go combat/quests), inspired by Elite but not a copy of
|
||||||
it; 8 branching story quests, save file (no bots —
|
it; 8 branching story quests, save file (no bots —
|
||||||
opponents inside the game are one-off encounters
|
opponents inside the game are one-off encounters
|
||||||
during travel, not persistent rivals)
|
during travel, not persistent rivals);
|
||||||
|
wayfarer_quest_loader.go can load additional quests
|
||||||
|
from .yaml files in a quests/ directory — see
|
||||||
|
example_quests/
|
||||||
|
third_party/yaml.v3 — YAML parsing library (github.com/go-yaml/yaml),
|
||||||
|
manually vendored locally because the sandbox's
|
||||||
|
network restrictions block the normal Go module
|
||||||
|
proxy (go.mod references it via a replace directive)
|
||||||
|
|
||||||
*_test.go — tests (rules unit tests + stress simulations of
|
*_test.go — tests (rules unit tests + stress simulations of
|
||||||
full bot games)
|
full bot games)
|
||||||
|
|
@ -129,7 +136,7 @@ go test ./... # the whole package
|
||||||
go test ./... -v # verbose output for every test
|
go test ./... -v # verbose output for every test
|
||||||
```
|
```
|
||||||
|
|
||||||
The package has 439 tests: unit tests for game logic (dealing, moves, scoring, edge cases like king captures, a marriage on the first trick, castling out of check or a pinned piece, an actual ko diagram in Go, chording in Minesweeper, full playability of all 8 Wayfarer quests) and separate stress tests that run hundreds to thousands of simulated bot games in a row — these are specifically written to catch rare hangs, card loss/duplication, and other subtle bugs that aren't always visible in a single manual test.
|
The package has 487 tests: unit tests for game logic (dealing, moves, scoring, edge cases like king captures, a marriage on the first trick, castling out of check or a pinned piece, an actual ko diagram in Go, chording in Minesweeper, full playability of all 8 Wayfarer quests) and separate stress tests that run hundreds to thousands of simulated bot games in a row — these are specifically written to catch rare hangs, card loss/duplication, and other subtle bugs that aren't always visible in a single manual test.
|
||||||
|
|
||||||
## Known simplifications
|
## Known simplifications
|
||||||
|
|
||||||
|
|
@ -144,4 +151,4 @@ Traditional card games usually have many regional rule variants. Where a choice
|
||||||
- **Corpse-Starch Box** — not a card game, but an incremental (idle/clicker) genre in the spirit of Candy Box 2, reimagined in a grim far-future setting; the only game in the collection with a save file (auto-saving every 15 seconds and on quit), with an honest accrual of passive income for the time the game was closed.
|
- **Corpse-Starch Box** — not a card game, but an incremental (idle/clicker) genre in the spirit of Candy Box 2, reimagined in a grim far-future setting; the only game in the collection with a save file (auto-saving every 15 seconds and on quit), with an honest accrual of passive income for the time the game was closed.
|
||||||
- **Go** — the basic rules (liberties, capturing, suicidal moves, simple ko) are fully implemented; NOT implemented: a dead-stone-removal phase before scoring (you need to actually capture hopeless enemy stones before passing twice, or the score will be wrong) and triple position repetition (superko). The bot at all three difficulty levels is heuristic, with no lookahead search through the game tree: genuinely strong Go play requires Monte Carlo search or neural-network position evaluation, which is beyond the scope of this project.
|
- **Go** — the basic rules (liberties, capturing, suicidal moves, simple ko) are fully implemented; NOT implemented: a dead-stone-removal phase before scoring (you need to actually capture hopeless enemy stones before passing twice, or the score will be wrong) and triple position repetition (superko). The bot at all three difficulty levels is heuristic, with no lookahead search through the game tree: genuinely strong Go play requires Monte Carlo search or neural-network position evaluation, which is beyond the scope of this project.
|
||||||
- **Minesweeper** — the first move is guaranteed safe not just for itself but for all its neighbors too (a bit more generous than the classic rule, which only excludes the clicked cell itself) — done deliberately so the first move usually opens up a noticeable area right away instead of a single bare number.
|
- **Minesweeper** — the first move is guaranteed safe not just for itself but for all its neighbors too (a bit more generous than the classic rule, which only excludes the clicked cell itself) — done deliberately so the first move usually opens up a noticeable area right away instead of a single bare number.
|
||||||
- **Wayfarer** — inspired by Elite, but deliberately not a copy of it: its own (not byte-for-byte) galaxy generation algorithm, and no honest real-time 3D flight — travel and dangerous encounters are resolved through text with choices instead of flying a cockpit. The name and all terminology are original, to avoid overlapping with the Elite trademark. Easter egg: just like the original, where you could change the generator's seed numbers in the code, here you can regenerate the entire galaxy by creating a `.env` file next to the executable with the line `WAYFARER_GALAXY_SEED=<number>` — no code changes needed.
|
- **Wayfarer** — inspired by Elite, but deliberately not a copy of it: its own (not byte-for-byte) galaxy generation algorithm, and no honest real-time 3D flight — travel and dangerous encounters are resolved through text with choices instead of flying a cockpit. The name and all terminology are original, to avoid overlapping with the Elite trademark. Easter egg: just like the original, where you could change the generator's seed numbers in the code, here you can regenerate the entire galaxy by creating a `.env` file next to the executable with the line `WAYFARER_GALAXY_SEED=<number>` — no code changes needed. Beyond the 8 built-in quests, you can add your own via `.yaml` files in a `quests/` directory — see `example_quests/lost_satellite.yaml` for an example and detailed format description.
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -115,7 +115,13 @@ wayfarer_*.go, — Странник: текстовая космото
|
||||||
translations_wayfarer*.go бой/квесты), вдохновлена Elite, но не копирует её; 8
|
translations_wayfarer*.go бой/квесты), вдохновлена Elite, но не копирует её; 8
|
||||||
сюжетных квестов с ветвлениями, сохранение на диск
|
сюжетных квестов с ветвлениями, сохранение на диск
|
||||||
(без ботов — соперники внутри игры не постоянные, а
|
(без ботов — соперники внутри игры не постоянные, а
|
||||||
разовые встречи в пути)
|
разовые встречи в пути); wayfarer_quest_loader.go
|
||||||
|
умеет подгружать дополнительные квесты из .yaml
|
||||||
|
файлов в директории quests/ — см. example_quests/
|
||||||
|
third_party/yaml.v3 — библиотека разбора YAML (github.com/go-yaml/yaml),
|
||||||
|
вручную вендорена локально из-за сетевых ограничений
|
||||||
|
песочницы на обычный Go-прокси (go.mod ссылается на
|
||||||
|
неё через replace)
|
||||||
|
|
||||||
*_test.go — тесты (юнит-тесты правил + стресс-симуляции
|
*_test.go — тесты (юнит-тесты правил + стресс-симуляции
|
||||||
полных партий ботами)
|
полных партий ботами)
|
||||||
|
|
@ -128,7 +134,7 @@ go test ./... # весь пакет
|
||||||
go test ./... -v # подробный вывод по каждому тесту
|
go test ./... -v # подробный вывод по каждому тесту
|
||||||
```
|
```
|
||||||
|
|
||||||
В пакете 439 тестов: юнит-тесты игровой логики (раздача, ходы, подсчёт очков, крайние случаи вроде взятия дамкой, марьяжа на первой взятке, рокировки под шахом или связанной фигуры, реальной диаграммы ко в Го, хорды в Сапёре, полной прохождимости всех 8 квестов Странника) и отдельные стресс-тесты, прогоняющие сотни-тысячи симулированных партий ботами подряд — они специально написаны, чтобы ловить редкие зависания, потерю/дублирование карт и прочие тонкие баги, которые не всегда видны в единичном ручном тесте.
|
В пакете 487 тестов: юнит-тесты игровой логики (раздача, ходы, подсчёт очков, крайние случаи вроде взятия дамкой, марьяжа на первой взятке, рокировки под шахом или связанной фигуры, реальной диаграммы ко в Го, хорды в Сапёре, полной прохождимости всех 8 квестов Странника) и отдельные стресс-тесты, прогоняющие сотни-тысячи симулированных партий ботами подряд — они специально написаны, чтобы ловить редкие зависания, потерю/дублирование карт и прочие тонкие баги, которые не всегда видны в единичном ручном тесте.
|
||||||
|
|
||||||
## Известные упрощения
|
## Известные упрощения
|
||||||
|
|
||||||
|
|
@ -143,4 +149,4 @@ go test ./... -v # подробный вывод по каждому те
|
||||||
- **Трупные батончики** — не карточная игра, а инкрементальный (idle/clicker) жанр в духе Candy Box 2, переосмысленный в антураже мрачного далёкого будущего; единственная игра коллекции с сохранением на диск (автоматически каждые 15 секунд и при выходе), с честным начислением пассивного дохода за время, пока партия была закрыта.
|
- **Трупные батончики** — не карточная игра, а инкрементальный (idle/clicker) жанр в духе Candy Box 2, переосмысленный в антураже мрачного далёкого будущего; единственная игра коллекции с сохранением на диск (автоматически каждые 15 секунд и при выходе), с честным начислением пассивного дохода за время, пока партия была закрыта.
|
||||||
- **Го** — базовые правила (свободы, взятие, самоубийственный ход, простое ко) реализованы полностью; НЕ реализованы: фаза удаления мёртвых камней перед подсчётом (нужно реально взять безнадёжные камни соперника до двух пасов подряд, иначе счёт будет неверным) и тройное повторение позиции (супер-ко). Бот на всех трёх уровнях сложности — эвристический, без поиска вперёд по дереву партии: честная сильная игра в Го требует Monte-Carlo поиска или нейросетевой оценки позиции, что выходит за рамки этого проекта.
|
- **Го** — базовые правила (свободы, взятие, самоубийственный ход, простое ко) реализованы полностью; НЕ реализованы: фаза удаления мёртвых камней перед подсчётом (нужно реально взять безнадёжные камни соперника до двух пасов подряд, иначе счёт будет неверным) и тройное повторение позиции (супер-ко). Бот на всех трёх уровнях сложности — эвристический, без поиска вперёд по дереву партии: честная сильная игра в Го требует Monte-Carlo поиска или нейросетевой оценки позиции, что выходит за рамки этого проекта.
|
||||||
- **Сапёр** — первый ход гарантированно безопасен не только сам по себе, но и все его соседи (чуть щедрее классического правила, где исключается только сама кликнутая клетка) — сделано специально, чтобы первый ход обычно сразу открывал заметную область, а не голую цифру.
|
- **Сапёр** — первый ход гарантированно безопасен не только сам по себе, но и все его соседи (чуть щедрее классического правила, где исключается только сама кликнутая клетка) — сделано специально, чтобы первый ход обычно сразу открывал заметную область, а не голую цифру.
|
||||||
- **Странник** — вдохновлён Elite, но сознательно не копирует её: собственный (не побайтовый) алгоритм генерации галактики, честного 3D-полёта в реальном времени нет — перелёты и опасные встречи разрешаются текстом с выбором вместо полёта в кокпите. Название и вся терминология авторские, чтобы не пересекаться с товарным знаком Elite. Пасхалка: как и в оригинале, где числа генератора можно было поменять в коде, здесь всю галактику можно перегенерировать заново, создав `.env` рядом с исполняемым файлом со строкой `WAYFARER_GALAXY_SEED=<число>` — без правки кода коллекции.
|
- **Странник** — вдохновлён Elite, но сознательно не копирует её: собственный (не побайтовый) алгоритм генерации галактики, честного 3D-полёта в реальном времени нет — перелёты и опасные встречи разрешаются текстом с выбором вместо полёта в кокпите. Название и вся терминология авторские, чтобы не пересекаться с товарным знаком Elite. Пасхалка: как и в оригинале, где числа генератора можно было поменять в коде, здесь всю галактику можно перегенерировать заново, создав `.env` рядом с исполняемым файлом со строкой `WAYFARER_GALAXY_SEED=<число>` — без правки кода коллекции. Помимо 8 встроенных квестов можно добавлять собственные через `.yaml` файлы в директории `quests/` — см. `example_quests/lost_satellite.yaml` за примером и подробным описанием формата.
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ type CorpseStarchModel struct {
|
||||||
|
|
||||||
message string
|
message string
|
||||||
|
|
||||||
|
confirmingReset bool // ждём подтверждения "начать заново" (y/n) — необратимое действие, требует явного согласия
|
||||||
|
|
||||||
quitting bool
|
quitting bool
|
||||||
backToMenu bool
|
backToMenu bool
|
||||||
}
|
}
|
||||||
|
|
@ -192,11 +194,30 @@ func (m CorpseStarchModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
m.quitting = true
|
m.quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.confirmingReset {
|
||||||
|
switch key {
|
||||||
|
case "y", "Y":
|
||||||
|
_ = DeleteCorpseStarchSave()
|
||||||
|
m.game = NewCorpseStarchGame(time.Now())
|
||||||
|
m.confirmingReset = false
|
||||||
|
m.setInfo("corpsestarch.msg.reset_done")
|
||||||
|
case "n", "N", "esc":
|
||||||
|
m.confirmingReset = false
|
||||||
|
m.message = ""
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
if key == "q" {
|
if key == "q" {
|
||||||
_ = SaveCorpseStarchGame(m.game)
|
_ = SaveCorpseStarchGame(m.game)
|
||||||
m.backToMenu = true
|
m.backToMenu = true
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
if key == "N" {
|
||||||
|
m.confirmingReset = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
actions := m.buildActions(now)
|
actions := m.buildActions(now)
|
||||||
|
|
@ -241,6 +262,14 @@ func (m CorpseStarchModel) View() string {
|
||||||
|
|
||||||
fmt.Fprintln(&b, titleStyle.Render(T("corpsestarch.title")))
|
fmt.Fprintln(&b, titleStyle.Render(T("corpsestarch.title")))
|
||||||
fmt.Fprintln(&b)
|
fmt.Fprintln(&b)
|
||||||
|
|
||||||
|
if m.confirmingReset {
|
||||||
|
fmt.Fprintln(&b, errorStyle.Render(T("corpsestarch.confirm_reset")))
|
||||||
|
fmt.Fprintln(&b)
|
||||||
|
fmt.Fprintln(&b, dimStyle.Render(T("corpsestarch.confirm_reset.hint")))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintln(&b, Tf("corpsestarch.header.starch", formatStarch(g.Starch), formatStarch(g.TotalEarned)))
|
fmt.Fprintln(&b, Tf("corpsestarch.header.starch", formatStarch(g.Starch), formatStarch(g.TotalEarned)))
|
||||||
if g.SumpUnlocked() {
|
if g.SumpUnlocked() {
|
||||||
fmt.Fprintln(&b, Tf("corpsestarch.header.corruption", g.Corruption))
|
fmt.Fprintln(&b, Tf("corpsestarch.header.corruption", g.Corruption))
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,69 @@ func TestCorpseStarchModelTickAccruesIncome(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCorpseStarchModelResetRequestShowsConfirmation(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewCorpseStarchModel()
|
||||||
|
next, _ := m.Update(key("N"))
|
||||||
|
m = next.(CorpseStarchModel)
|
||||||
|
if !m.confirmingReset {
|
||||||
|
t.Fatalf("ожидался запрос подтверждения после нажатия N")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorpseStarchModelResetCancelKeepsProgress(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewCorpseStarchModel()
|
||||||
|
m.game.Starch = 500
|
||||||
|
m.game.TotalEarned = 500
|
||||||
|
|
||||||
|
next, _ := m.Update(key("N"))
|
||||||
|
m = next.(CorpseStarchModel)
|
||||||
|
next, _ = m.Update(key("n"))
|
||||||
|
m = next.(CorpseStarchModel)
|
||||||
|
|
||||||
|
if m.confirmingReset {
|
||||||
|
t.Errorf("запрос подтверждения должен был закрыться после отмены")
|
||||||
|
}
|
||||||
|
if m.game.Starch != 500 {
|
||||||
|
t.Errorf("прогресс не должен был сброситься при отмене, получено Starch=%v", m.game.Starch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorpseStarchModelResetConfirmWipesProgress(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewCorpseStarchModel()
|
||||||
|
m.game.Starch = 500
|
||||||
|
m.game.TotalEarned = 500
|
||||||
|
|
||||||
|
next, _ := m.Update(key("N"))
|
||||||
|
m = next.(CorpseStarchModel)
|
||||||
|
next, _ = m.Update(key("y"))
|
||||||
|
m = next.(CorpseStarchModel)
|
||||||
|
|
||||||
|
if m.confirmingReset {
|
||||||
|
t.Errorf("запрос подтверждения должен был закрыться после подтверждения")
|
||||||
|
}
|
||||||
|
if m.game.Starch != 0 || m.game.TotalEarned != 0 {
|
||||||
|
t.Errorf("прогресс должен был сброситься, получено Starch=%v TotalEarned=%v", m.game.Starch, m.game.TotalEarned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorpseStarchModelResetActionsIgnoredWhileConfirming(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewCorpseStarchModel()
|
||||||
|
starchBefore := m.game.Starch
|
||||||
|
|
||||||
|
next, _ := m.Update(key("N"))
|
||||||
|
m = next.(CorpseStarchModel)
|
||||||
|
// нажатие "1" (реквизиция) не должно срабатывать, пока ждём подтверждения
|
||||||
|
next, _ = m.Update(key("1"))
|
||||||
|
m = next.(CorpseStarchModel)
|
||||||
|
if m.game.Starch != starchBefore {
|
||||||
|
t.Errorf("обычные действия не должны выполняться во время запроса подтверждения")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCorpseStarchModelQReturnsToMenu(t *testing.T) {
|
func TestCorpseStarchModelQReturnsToMenu(t *testing.T) {
|
||||||
withTempConfigDir(t)
|
withTempConfigDir(t)
|
||||||
m := NewCorpseStarchModel()
|
m := NewCorpseStarchModel()
|
||||||
|
|
|
||||||
14
deck.go
14
deck.go
|
|
@ -55,11 +55,23 @@ func (d *Deck) Draw() (Card, bool) {
|
||||||
// DrawN снимает n верхних карт (используется при раздаче).
|
// DrawN снимает n верхних карт (используется при раздаче).
|
||||||
// Если в колоде меньше карт, чем запрошено, возвращает столько,
|
// Если в колоде меньше карт, чем запрошено, возвращает столько,
|
||||||
// сколько есть.
|
// сколько есть.
|
||||||
|
//
|
||||||
|
// ВАЖНО: возвращает независимую копию, а не подсрез d.cards. Раньше
|
||||||
|
// здесь было "out := d.cards[:n]" — такой подсрез имеет ту же
|
||||||
|
// вместимость (cap), что и исходный массив колоды, вплоть до самого
|
||||||
|
// его конца. Это означало, что руки РАЗНЫХ игроков после раздачи
|
||||||
|
// физически указывали на один и тот же нижележащий массив: как
|
||||||
|
// только первый игрок брал карту (append к своей руке), Go писал
|
||||||
|
// новый элемент прямо в этот общий массив — по адресу, где у
|
||||||
|
// СЛЕДУЮЩЕГО игрока лежала его собственная карта, молча её затирая.
|
||||||
|
// Тот самый баг с "одна и та же карта одновременно в сбросе и в
|
||||||
|
// комбинации на столе" — именно отсюда.
|
||||||
func (d *Deck) DrawN(n int) []Card {
|
func (d *Deck) DrawN(n int) []Card {
|
||||||
if n > len(d.cards) {
|
if n > len(d.cards) {
|
||||||
n = len(d.cards)
|
n = len(d.cards)
|
||||||
}
|
}
|
||||||
out := d.cards[:n]
|
out := make([]Card, n)
|
||||||
|
copy(out, d.cards[:n])
|
||||||
d.cards = d.cards[n:]
|
d.cards = d.cards[n:]
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
deck_test.go
Normal file
70
deck_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestDrawNReturnsIndependentSlice — прицельный регрессионный тест
|
||||||
|
// на реальный найденный баг: DrawN раньше возвращал подсрез самой
|
||||||
|
// колоды (out := d.cards[:n]), у которого вместимость (cap)
|
||||||
|
// простиралась до конца массива колоды. Это означало, что append к
|
||||||
|
// возвращённой "руке" писал прямо в общую с колодой (и с ранее
|
||||||
|
// розданными руками) память. Тест явно проверяет: после DrawN и
|
||||||
|
// последующего append к результату, ни оставшаяся колода, ни ранее
|
||||||
|
// выданная другая рука не должны увидеть "чужую" карту.
|
||||||
|
func TestDrawNReturnsIndependentSlice(t *testing.T) {
|
||||||
|
d := NewDeck() // 52 карты по порядку, без перемешивания — детерминированно для теста
|
||||||
|
|
||||||
|
hand1 := d.DrawN(5)
|
||||||
|
hand2 := d.DrawN(5)
|
||||||
|
hand2Before := append([]Card{}, hand2...)
|
||||||
|
deckBefore := append([]Card{}, d.cards...)
|
||||||
|
|
||||||
|
// имитируем "взятие карты" (append к руке первого игрока), как
|
||||||
|
// это происходит в реальной игре при добавлении карты с добора
|
||||||
|
hand1 = append(hand1, Card{Suit: Spades, Rank: Ace})
|
||||||
|
_ = hand1
|
||||||
|
|
||||||
|
for i := range d.cards {
|
||||||
|
if d.cards[i] != deckBefore[i] {
|
||||||
|
t.Fatalf("append к первой руке испортил оставшуюся колоду на позиции %d: было %v, стало %v",
|
||||||
|
i, deckBefore[i], d.cards[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, c := range hand2 {
|
||||||
|
if c != hand2Before[i] {
|
||||||
|
t.Fatalf("append к первой руке исказил вторую руку на позиции %d: было %v, стало %v",
|
||||||
|
i, hand2Before[i], c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDrawNSequentialHandsDoNotAlias — то же самое, но раскладывает
|
||||||
|
// сразу на 4 "игроков" подряд (как при реальной раздаче в newRound)
|
||||||
|
// и проверяет, что рост ЛЮБОЙ из рук через append не портит
|
||||||
|
// остальные, ещё не выросшие руки.
|
||||||
|
func TestDrawNSequentialHandsDoNotAlias(t *testing.T) {
|
||||||
|
d := NewDeck()
|
||||||
|
hands := make([][]Card, 4)
|
||||||
|
for i := range hands {
|
||||||
|
hands[i] = d.DrawN(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots := make([][]Card, 4)
|
||||||
|
for i, h := range hands {
|
||||||
|
snapshots[i] = append([]Card{}, h...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range hands {
|
||||||
|
hands[i] = append(hands[i], Card{Suit: Hearts, Rank: Ace})
|
||||||
|
for j := range hands {
|
||||||
|
if j == i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for k, c := range snapshots[j] {
|
||||||
|
if hands[j][k] != c {
|
||||||
|
t.Fatalf("рост руки %d испортил руку %d на позиции %d: было %v, стало %v",
|
||||||
|
i, j, k, c, hands[j][k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
example_quests/lost_satellite.yaml
Normal file
65
example_quests/lost_satellite.yaml
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Пример пользовательского квеста для "Странника".
|
||||||
|
#
|
||||||
|
# Чтобы этот квест реально появился в игре — скопируйте этот файл в
|
||||||
|
# директорию "quests/" рядом с исполняемым файлом игры (или в
|
||||||
|
# директорию, откуда вы её запускаете), и перезапустите программу.
|
||||||
|
# Никакой пересборки не требуется.
|
||||||
|
#
|
||||||
|
# Правила:
|
||||||
|
# - key должен быть уникальным (не совпадать со встроенными
|
||||||
|
# квестами и другими вашими .yaml файлами).
|
||||||
|
# - У каждого шага (steps) должен быть текст (text) и хотя бы
|
||||||
|
# один вариант выбора (choices).
|
||||||
|
# - У каждого варианта выбора: либо next_step (индекс следующего
|
||||||
|
# шага, начиная с 0), либо terminal: true (тогда квест
|
||||||
|
# заканчивается этим выбором — успехом при success: true, или
|
||||||
|
# провалом при success: false — и обязательно нужен result).
|
||||||
|
# - credits_delta / hull_delta / kills_delta — необязательные
|
||||||
|
# числовые последствия (могут быть отрицательными).
|
||||||
|
# - cargo_idx (0-9, индекс в списке товаров) и cargo_delta —
|
||||||
|
# тоже необязательны; указывайте оба вместе, если выбор
|
||||||
|
# должен добавить или отнять что-то из трюма.
|
||||||
|
# - Каждый текст обязательно должен быть заполнен на ОБА языка
|
||||||
|
# (ru и en) — иначе файл целиком будет пропущен с
|
||||||
|
# предупреждением при запуске (сообщение выводится в stderr).
|
||||||
|
|
||||||
|
key: quest.lost_satellite
|
||||||
|
title:
|
||||||
|
ru: "Пропавший спутник"
|
||||||
|
en: "The Lost Satellite"
|
||||||
|
brief:
|
||||||
|
ru: "Найти потерянный исследовательский зонд"
|
||||||
|
en: "Find a lost research probe"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- text:
|
||||||
|
ru: "Диспетчер станции просит помочь найти пропавший исследовательский спутник — сигнал слабый, но ещё есть."
|
||||||
|
en: "The station dispatcher asks for help finding a missing research satellite — the signal is weak, but still there."
|
||||||
|
choices:
|
||||||
|
- label:
|
||||||
|
ru: "Отправиться на поиски"
|
||||||
|
en: "Go looking for it"
|
||||||
|
next_step: 1
|
||||||
|
- label:
|
||||||
|
ru: "Не наше дело"
|
||||||
|
en: "Not our problem"
|
||||||
|
terminal: true
|
||||||
|
success: false
|
||||||
|
result:
|
||||||
|
ru: "Вы отказываетесь и летите дальше своим курсом."
|
||||||
|
en: "You decline and continue on your way."
|
||||||
|
|
||||||
|
- text:
|
||||||
|
ru: "Спутник найден дрейфующим у астероида — повреждён, но данные внутри целы."
|
||||||
|
en: "The satellite is found drifting near an asteroid — damaged, but the data inside is intact."
|
||||||
|
choices:
|
||||||
|
- label:
|
||||||
|
ru: "Забрать спутник и вернуть на станцию"
|
||||||
|
en: "Retrieve it and bring it back"
|
||||||
|
terminal: true
|
||||||
|
success: true
|
||||||
|
result:
|
||||||
|
ru: "Диспетчер щедро благодарит за возвращённые данные многолетних наблюдений."
|
||||||
|
en: "The dispatcher generously rewards you for the years of returned observation data."
|
||||||
|
credits_delta: 900
|
||||||
|
hull_delta: -10
|
||||||
95
game_test.go
95
game_test.go
|
|
@ -1,6 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestNewGameDealsCorrectly(t *testing.T) {
|
func TestNewGameDealsCorrectly(t *testing.T) {
|
||||||
g := NewGame([]string{"A", "B", "C"}, -1)
|
g := NewGame([]string{"A", "B", "C"}, -1)
|
||||||
|
|
@ -315,6 +319,95 @@ func TestLayNewMeldAndAddToMeld(t *testing.T) {
|
||||||
// трёх уровней в разных сочетаниях и проверяет, что игра всегда
|
// трёх уровней в разных сочетаниях и проверяет, что игра всегда
|
||||||
// корректно завершается (без паники и без зависания) и что очки
|
// корректно завершается (без паники и без зависания) и что очки
|
||||||
// нулевой суммы (кроме мёртвых раундов).
|
// нулевой суммы (кроме мёртвых раундов).
|
||||||
|
// allCardsAccountedFor проверяет, что все 52 карты стандартной
|
||||||
|
// колоды встречаются в игре РОВНО один раз суммарно по: колоде,
|
||||||
|
// сбросу, рукам игроков и комбинациям на столе. Возвращает список
|
||||||
|
// описаний найденных нарушений (пустой — всё в порядке).
|
||||||
|
func allCardsAccountedFor(g *GameState) []string {
|
||||||
|
counts := map[Card]int{}
|
||||||
|
locations := map[Card][]string{}
|
||||||
|
|
||||||
|
record := func(c Card, where string) {
|
||||||
|
counts[c]++
|
||||||
|
locations[c] = append(locations[c], where)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range g.Deck.cards {
|
||||||
|
record(c, "колода")
|
||||||
|
}
|
||||||
|
for _, c := range g.DiscardPile {
|
||||||
|
record(c, "сброс")
|
||||||
|
}
|
||||||
|
for i, p := range g.Players {
|
||||||
|
for _, c := range p.Hand {
|
||||||
|
record(c, fmt.Sprintf("рука игрока %d", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for mi, meld := range g.Table {
|
||||||
|
for _, c := range meld.Cards {
|
||||||
|
record(c, fmt.Sprintf("комбинация №%d на столе", mi))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var problems []string
|
||||||
|
fullDeck := NewDeck()
|
||||||
|
for _, c := range fullDeck.cards {
|
||||||
|
switch counts[c] {
|
||||||
|
case 1:
|
||||||
|
case 0:
|
||||||
|
problems = append(problems, fmt.Sprintf("карта %s пропала — нигде не найдена", c))
|
||||||
|
default:
|
||||||
|
problems = append(problems, fmt.Sprintf("карта %s встречается %d раз(а) — в: %v", c, counts[c], locations[c]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCardIntegrityAcrossFullBotGames — по мотивам реального
|
||||||
|
// наблюдения пользователя (одна и та же карта якобы одновременно
|
||||||
|
// в сбросе и в комбинации на столе): прогоняет много полных партий
|
||||||
|
// разными сочетаниями ботов, проверяя после КАЖДОГО хода, что все
|
||||||
|
// 52 карты стандартной колоды учтены ровно по одному разу — нигде
|
||||||
|
// не потеряны и нигде не задублированы.
|
||||||
|
func TestCardIntegrityAcrossFullBotGames(t *testing.T) {
|
||||||
|
configs := [][]BotStrategy{
|
||||||
|
{SimpleBot{}, SimpleBot{}, SimpleBot{}, SimpleBot{}},
|
||||||
|
{MediumBot{}, MediumBot{}, MediumBot{}, MediumBot{}},
|
||||||
|
{TonkBot{}, TonkBot{}, TonkBot{}, TonkBot{}},
|
||||||
|
{SimpleBot{}, MediumBot{}, TonkBot{}, SimpleBot{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for cfgIdx, bots := range configs {
|
||||||
|
for game := 0; game < 50; game++ {
|
||||||
|
names := make([]string, len(bots))
|
||||||
|
for i := range names {
|
||||||
|
names[i] = "Bot"
|
||||||
|
}
|
||||||
|
g := NewGame(names, -1)
|
||||||
|
|
||||||
|
const maxTurns = 1000
|
||||||
|
for turn := 0; turn < maxTurns && g.Phase != PhaseGameOver; turn++ {
|
||||||
|
bot := bots[g.CurrentPlayer]
|
||||||
|
if err := bot.PlayTurn(g); err != nil {
|
||||||
|
t.Fatalf("конфиг %d, игра %d, ход %d: ошибка хода бота: %v", cfgIdx, game, turn, err)
|
||||||
|
}
|
||||||
|
if problems := allCardsAccountedFor(g); len(problems) > 0 {
|
||||||
|
t.Fatalf("конфиг %d, игра %d, ход %d: нарушена целостность колоды:\n%s",
|
||||||
|
cfgIdx, game, turn, strings.Join(problems, "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Phase == PhaseGameOver {
|
||||||
|
g2 := g.NextRound()
|
||||||
|
if problems := allCardsAccountedFor(g2); len(problems) > 0 {
|
||||||
|
t.Fatalf("конфиг %d, игра %d: нарушена целостность колоды сразу после NextRound():\n%s",
|
||||||
|
cfgIdx, game, strings.Join(problems, "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFullBotGames(t *testing.T) {
|
func TestFullBotGames(t *testing.T) {
|
||||||
configs := [][]BotStrategy{
|
configs := [][]BotStrategy{
|
||||||
{SimpleBot{}, SimpleBot{}, SimpleBot{}, SimpleBot{}},
|
{SimpleBot{}, SimpleBot{}, SimpleBot{}, SimpleBot{}},
|
||||||
|
|
|
||||||
BIN
ggc
(Stored with Git LFS)
BIN
ggc
(Stored with Git LFS)
Binary file not shown.
3
go.mod
3
go.mod
|
|
@ -8,10 +8,13 @@ replace golang.org/x/text => github.com/golang/text v0.3.8
|
||||||
|
|
||||||
replace golang.org/x/sync => github.com/golang/sync v0.9.0
|
replace golang.org/x/sync => github.com/golang/sync v0.9.0
|
||||||
|
|
||||||
|
replace gopkg.in/yaml.v3 => ./third_party/yaml.v3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/bubbletea v1.2.4
|
github.com/charmbracelet/bubbletea v1.2.4
|
||||||
github.com/charmbracelet/lipgloss v1.0.0
|
github.com/charmbracelet/lipgloss v1.0.0
|
||||||
github.com/muesli/termenv v0.15.2
|
github.com/muesli/termenv v0.15.2
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
|
||||||
|
|
@ -92,12 +92,15 @@ func goBoardOccupancy(g *GoGameState) float64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayFullTurn исполняет один ход бота (игру в точку либо пас).
|
// PlayFullTurn исполняет один ход бота (игру в точку либо пас).
|
||||||
func (b GoBot) PlayFullTurn(g *GoGameState, rnd *rand.Rand) error {
|
// Возвращает passed=true, если бот решил пасовать — вызывающий код
|
||||||
|
// (TUI) должен показать это явно, а не молчать, будто ничего не
|
||||||
|
// произошло.
|
||||||
|
func (b GoBot) PlayFullTurn(g *GoGameState, rnd *rand.Rand) (passed bool, err error) {
|
||||||
pos, ok := b.DecideMove(g, rnd)
|
pos, ok := b.DecideMove(g, rnd)
|
||||||
if !ok {
|
if !ok {
|
||||||
return g.Pass()
|
return true, g.Pass()
|
||||||
}
|
}
|
||||||
return g.Play(pos)
|
return false, g.Play(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// goLegalMoves перечисляет все легальные точки для текущего игрока.
|
// goLegalMoves перечисляет все легальные точки для текущего игрока.
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,46 @@ func TestGoLegalMoveEnumerationPerformance19x19(t *testing.T) {
|
||||||
// TestGoBotFullGames прогоняет несколько полных партий бота против
|
// TestGoBotFullGames прогоняет несколько полных партий бота против
|
||||||
// самого себя на каждом размере доски и уровне сложности, проверяя
|
// самого себя на каждом размере доски и уровне сложности, проверяя
|
||||||
// отсутствие паник, зависаний и корректность итогового результата.
|
// отсутствие паник, зависаний и корректность итогового результата.
|
||||||
|
// TestGoPlayedStonePersistsAcrossManyGames — по мотивам реального
|
||||||
|
// наблюдения пользователя ("бот съел свой камень, когда окружил"):
|
||||||
|
// после ЛЮБОГО успешного (не отклонённого как самоубийство или
|
||||||
|
// нарушение ко) хода поставленный камень обязан остаться на доске
|
||||||
|
// своим цветом — если бы он тут же исчез, это означало бы, что либо
|
||||||
|
// сам ход был на самом деле самоубийственным (баг проверки суицида),
|
||||||
|
// либо взятие каким-то образом задело группу самого ходящего вместо
|
||||||
|
// вражеской. Прогоняет много партий бот-против-бота на всех
|
||||||
|
// размерах доски и уровнях сложности, проверяя инвариант после
|
||||||
|
// КАЖДОГО хода.
|
||||||
|
func TestGoPlayedStonePersistsAcrossManyGames(t *testing.T) {
|
||||||
|
rnd := rand.New(rand.NewSource(7))
|
||||||
|
|
||||||
|
for _, size := range []int{9, 13, 19} {
|
||||||
|
for _, diff := range []GoDifficulty{GoDifficultyEasy, GoDifficultyMedium, GoDifficultyHard} {
|
||||||
|
bot := GoBot{Difficulty: diff}
|
||||||
|
g := NewGoGame(size)
|
||||||
|
|
||||||
|
const maxMoves = 300
|
||||||
|
for move := 0; move < maxMoves && g.Result == nil; move++ {
|
||||||
|
mover := g.Turn
|
||||||
|
pos, ok := bot.DecideMove(g, rnd)
|
||||||
|
if !ok {
|
||||||
|
if err := g.Pass(); err != nil {
|
||||||
|
t.Fatalf("размер %d, сложность %d, ход %d: неожиданная ошибка паса: %v", size, diff, move, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := g.Play(pos); err != nil {
|
||||||
|
t.Fatalf("размер %d, сложность %d, ход %d: неожиданная ошибка хода в %v: %v", size, diff, move, pos, err)
|
||||||
|
}
|
||||||
|
if g.Board[pos.Row][pos.Col] != mover {
|
||||||
|
t.Fatalf("размер %d, сложность %d, ход %d: после успешного хода цвета %v в точку %v там оказалось %v — камень исчез или сменил цвет сразу после собственного хода",
|
||||||
|
size, diff, move, mover, pos, g.Board[pos.Row][pos.Col])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGoBotFullGames(t *testing.T) {
|
func TestGoBotFullGames(t *testing.T) {
|
||||||
rnd := rand.New(rand.NewSource(3))
|
rnd := rand.New(rand.NewSource(3))
|
||||||
|
|
||||||
|
|
@ -83,7 +123,7 @@ func TestGoBotFullGames(t *testing.T) {
|
||||||
const maxMoves = 800
|
const maxMoves = 800
|
||||||
moves := 0
|
moves := 0
|
||||||
for ; moves < maxMoves && g.Result == nil; moves++ {
|
for ; moves < maxMoves && g.Result == nil; moves++ {
|
||||||
if err := bot.PlayFullTurn(g, rnd); err != nil {
|
if _, err := bot.PlayFullTurn(g, rnd); err != nil {
|
||||||
t.Fatalf("размер %d, сложность %d, ход %d: неожиданная ошибка: %v", size, diff, moves, err)
|
t.Fatalf("размер %d, сложность %d, ход %d: неожиданная ошибка: %v", size, diff, moves, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,16 @@ func (m GoModel) handleBotMove() (tea.Model, tea.Cmd) {
|
||||||
if m.game.Result != nil || m.game.Turn == m.human {
|
if m.game.Result != nil || m.game.Turn == m.human {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if err := m.bot.PlayFullTurn(m.game, m.rnd); err != nil {
|
passed, err := m.bot.PlayFullTurn(m.game, m.rnd)
|
||||||
|
if err != nil {
|
||||||
m.setError(err)
|
m.setError(err)
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
m.setInfo("go.msg.bot_moved")
|
if passed {
|
||||||
|
m.setInfo("go.msg.bot_passed")
|
||||||
|
} else {
|
||||||
|
m.setInfo("go.msg.bot_moved")
|
||||||
|
}
|
||||||
return m, m.maybeScheduleBot()
|
return m, m.maybeScheduleBot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,36 @@ func TestGoModelInvalidMoveShowsError(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGoModelBotPassShowsDistinctMessage — по мотивам реального
|
||||||
|
// наблюдения пользователя: "бот делал ход, но камень не появлялся"
|
||||||
|
// — на самом деле бот пасовал, но интерфейс молча показывал то же
|
||||||
|
// сообщение "Бот сходил.", что и при обычном ходе, из-за чего
|
||||||
|
// казалось, будто ход просто не отрисовался.
|
||||||
|
func TestGoModelBotPassShowsDistinctMessage(t *testing.T) {
|
||||||
|
m := NewGoModel(9, GoDifficultyEasy)
|
||||||
|
// заполняем всю доску без единой пустой клетки — тогда легальных
|
||||||
|
// ходов гарантированно нет ни у кого, пас неизбежен
|
||||||
|
for r := 0; r < 9; r++ {
|
||||||
|
for c := 0; c < 9; c++ {
|
||||||
|
if (r+c)%2 == 0 {
|
||||||
|
m.game.Board[r][c] = GoBlack
|
||||||
|
} else {
|
||||||
|
m.game.Board[r][c] = GoWhite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.game.Turn = GoWhite
|
||||||
|
|
||||||
|
next, _ := m.Update(botMoveMsg{})
|
||||||
|
m = next.(GoModel)
|
||||||
|
if m.message != T("go.msg.bot_passed") {
|
||||||
|
t.Errorf("ожидалось отдельное сообщение о пасе бота, получено: %q", m.message)
|
||||||
|
}
|
||||||
|
if m.message == T("go.msg.bot_moved") {
|
||||||
|
t.Errorf("сообщение о пасе не должно совпадать с сообщением об обычном ходе")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGoModelQReturnsToMenu(t *testing.T) {
|
func TestGoModelQReturnsToMenu(t *testing.T) {
|
||||||
m := NewGoModel(9, GoDifficultyEasy)
|
m := NewGoModel(9, GoDifficultyEasy)
|
||||||
next, _ := m.Update(key("q"))
|
next, _ := m.Update(key("q"))
|
||||||
|
|
|
||||||
|
|
@ -252,11 +252,16 @@ func (m KlondikeModel) renderTableau() string {
|
||||||
cells[col] = dimStyle.Render("[?]")
|
cells[col] = dimStyle.Render("[?]")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
text := renderCard(kc.Card)
|
// паддинг до фиксированной ширины 3 (как у "[?]" и самой
|
||||||
|
// длинной карты "10♠") ДО применения цвета — иначе более
|
||||||
|
// короткие карты (например "A♠", 2 символа против 3 у
|
||||||
|
// "10♠") сдвигали бы соседние колонки в той же строке
|
||||||
|
padded := fmt.Sprintf("%-3s", kc.Card.String())
|
||||||
|
text := cardColorStyle(kc.Card).Render(padded)
|
||||||
isTopOfSelectedRun := m.selected == klondikePileRef(col) &&
|
isTopOfSelectedRun := m.selected == klondikePileRef(col) &&
|
||||||
row >= len(m.game.Tableau[col])-m.game.movableRun(col)
|
row >= len(m.game.Tableau[col])-m.game.movableRun(col)
|
||||||
if isTopOfSelectedRun {
|
if isTopOfSelectedRun {
|
||||||
text = cursorStyle.Render(kc.Card.String())
|
text = cursorStyle.Render(padded)
|
||||||
}
|
}
|
||||||
cells[col] = text
|
cells[col] = text
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,45 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ansiEscapeRe = regexp.MustCompile("\x1b\\[[0-9;]*m")
|
||||||
|
|
||||||
|
func stripANSI(s string) string {
|
||||||
|
return ansiEscapeRe.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestKlondikeTableauAlignmentWithMixedCardWidths проверяет
|
||||||
|
// исправление бага: карта "10" (единственный ранг длиной в 2
|
||||||
|
// символа) без паддинга оказывалась короче остальных карт и
|
||||||
|
// закрытой клетки "[?]", из-за чего колонки правее в той же строке
|
||||||
|
// сдвигались и вертикальное выравнивание таблицы ломалось.
|
||||||
|
func TestKlondikeTableauAlignmentWithMixedCardWidths(t *testing.T) {
|
||||||
|
m := NewKlondikeModel()
|
||||||
|
m.game.Tableau[0] = []KlondikeCard{{Card: Card{Rank: Ace, Suit: Spades}, FaceUp: true}}
|
||||||
|
m.game.Tableau[1] = []KlondikeCard{{Card: Card{Rank: Ten, Suit: Hearts}, FaceUp: true}}
|
||||||
|
m.game.Tableau[2] = []KlondikeCard{{Card: Card{Rank: King, Suit: Clubs}, FaceUp: false}}
|
||||||
|
m.game.Tableau[3] = []KlondikeCard{{Card: Card{Rank: Queen, Suit: Diamonds}, FaceUp: true}}
|
||||||
|
m.game.Tableau[4] = []KlondikeCard{}
|
||||||
|
m.game.Tableau[5] = []KlondikeCard{}
|
||||||
|
m.game.Tableau[6] = []KlondikeCard{}
|
||||||
|
|
||||||
|
rendered := stripANSI(m.renderTableau())
|
||||||
|
lines := strings.Split(rendered, "\n")
|
||||||
|
if len(lines) < 2 {
|
||||||
|
t.Fatalf("ожидалось минимум 2 строки (заголовок + ряд карт), получено %d", len(lines))
|
||||||
|
}
|
||||||
|
|
||||||
|
headerWidth := len([]rune(lines[0]))
|
||||||
|
for i, line := range lines[1:] {
|
||||||
|
if w := len([]rune(line)); w != headerWidth {
|
||||||
|
t.Errorf("строка %d имеет ширину %d, отличную от заголовка (%d): %q", i+1, w, headerWidth, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestKlondikeModelDrawViaKeys(t *testing.T) {
|
func TestKlondikeModelDrawViaKeys(t *testing.T) {
|
||||||
m := NewKlondikeModel()
|
m := NewKlondikeModel()
|
||||||
|
|
|
||||||
1
main.go
1
main.go
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
loadWayfarerGalaxySeed()
|
loadWayfarerGalaxySeed()
|
||||||
|
loadCustomWayfarerQuests()
|
||||||
|
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
|
|
|
||||||
45
rules.go
45
rules.go
|
|
@ -1124,6 +1124,8 @@ const corpseStarchRulesRU = `ТРУПНЫЕ БАТОНЧИКИ (CORPSE-STARCH BO
|
||||||
УПРАВЛЕНИЕ
|
УПРАВЛЕНИЕ
|
||||||
1-9 выбрать пронумерованное действие (список меняется по мере
|
1-9 выбрать пронумерованное действие (список меняется по мере
|
||||||
открытия новых разделов — номера могут "переезжать")
|
открытия новых разделов — номера могут "переезжать")
|
||||||
|
N начать заново (с подтверждением — весь текущий прогресс
|
||||||
|
будет безвозвратно потерян)
|
||||||
q сохранить партию и выйти в меню
|
q сохранить партию и выйти в меню
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -1168,6 +1170,8 @@ SECTIONS THAT UNLOCK AS YOU GATHER MORE
|
||||||
CONTROLS
|
CONTROLS
|
||||||
1-9 choose a numbered action (the list grows as you unlock more
|
1-9 choose a numbered action (the list grows as you unlock more
|
||||||
sections — numbers may shift around)
|
sections — numbers may shift around)
|
||||||
|
N start over (with confirmation — all current progress will
|
||||||
|
be permanently lost)
|
||||||
q save the game and quit to menu
|
q save the game and quit to menu
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -1482,9 +1486,12 @@ Braben, Ian Bell), но не являющаяся её копией: собст
|
||||||
10 видов товаров, включая два незаконных (контрабанда, оружие) —
|
10 видов товаров, включая два незаконных (контрабанда, оружие) —
|
||||||
их проверяют патрули в системах со строгим правительством. Цена
|
их проверяют патрули в системах со строгим правительством. Цена
|
||||||
зависит от типа экономики системы: аграрные дешевле продают
|
зависит от типа экономики системы: аграрные дешевле продают
|
||||||
продовольствие, промышленные — технику, и так далее. Пока товар
|
продовольствие, промышленные — технику, и так далее. Цены
|
||||||
лежит в трюме, рынок показывает среднюю цену, по которой он был
|
фиксируются в момент стыковки со станцией и не меняются, пока вы
|
||||||
куплен, — удобно сравнить с текущей ценой перед продажей.
|
там находитесь, — пересчитываются заново только при следующей
|
||||||
|
стыковке (после перелёта). Пока товар лежит в трюме, рынок
|
||||||
|
показывает среднюю цену, по которой он был куплен, — удобно
|
||||||
|
сравнить с текущей ценой перед продажей.
|
||||||
|
|
||||||
ПЕРЕЛЁТЫ И ОПАСНЫЕ ВСТРЕЧИ
|
ПЕРЕЛЁТЫ И ОПАСНЫЕ ВСТРЕЧИ
|
||||||
Прыжок в пределах галактики тратит топливо пропорционально
|
Прыжок в пределах галактики тратит топливо пропорционально
|
||||||
|
|
@ -1513,6 +1520,15 @@ Braben, Ian Bell), но не являющаяся её копией: собст
|
||||||
позже. Одновременно активно только одно задание; выбор в его ходе
|
позже. Одновременно активно только одно задание; выбор в его ходе
|
||||||
влияет на награду, здоровье корабля и иногда — на груз.
|
влияет на награду, здоровье корабля и иногда — на груз.
|
||||||
|
|
||||||
|
СОБСТВЕННЫЕ КВЕСТЫ: помимо 8 встроенных, можно добавить свои,
|
||||||
|
не трогая код игры — положите .yaml файл в директорию "quests"
|
||||||
|
рядом с исполняемым файлом (или в директорию, откуда запускаете
|
||||||
|
игру), и перезапустите программу. Формат файла и подробные
|
||||||
|
комментарии — в example_quests/lost_satellite.yaml. Файлы с
|
||||||
|
ошибками (плохой синтаксис, неполный перевод, конфликт ключа с
|
||||||
|
уже существующим квестом) просто пропускаются с предупреждением
|
||||||
|
в stderr — никогда не ломают игру целиком.
|
||||||
|
|
||||||
СНАРЯЖЕНИЕ И РЕМОНТ
|
СНАРЯЖЕНИЕ И РЕМОНТ
|
||||||
На верфи можно дозаправиться, починить корпус и один раз
|
На верфи можно дозаправиться, починить корпус и один раз
|
||||||
приобрести каждый апгрейд: топливный скиммер, расширение трюма,
|
приобрести каждый апгрейд: топливный скиммер, расширение трюма,
|
||||||
|
|
@ -1522,6 +1538,8 @@ Braben, Ian Bell), но не являющаяся её копией: собст
|
||||||
На станции (хаб):
|
На станции (хаб):
|
||||||
1 рынок 2 верфь 3 перелёт 4 квесты
|
1 рынок 2 верфь 3 перелёт 4 квесты
|
||||||
q сохранить карьеру и выйти в меню
|
q сохранить карьеру и выйти в меню
|
||||||
|
N начать новую карьеру (с подтверждением — текущая карьера
|
||||||
|
будет безвозвратно потеряна)
|
||||||
Рынок/верфь/перелёт/квесты:
|
Рынок/верфь/перелёт/квесты:
|
||||||
↑↓ / kj выбор пункта
|
↑↓ / kj выбор пункта
|
||||||
b / s купить/продать 1 единицу (на рынке)
|
b / s купить/продать 1 единицу (на рынке)
|
||||||
|
|
@ -1573,10 +1591,12 @@ TRADING
|
||||||
10 types of goods, including two illegal ones (contraband, firearms)
|
10 types of goods, including two illegal ones (contraband, firearms)
|
||||||
— patrols check for them in systems with strict governments. Prices
|
— patrols check for them in systems with strict governments. Prices
|
||||||
depend on the system's economy: agricultural systems sell food
|
depend on the system's economy: agricultural systems sell food
|
||||||
cheaply, industrial ones sell machinery cheaply, and so on. While a
|
cheaply, industrial ones sell machinery cheaply, and so on. Prices
|
||||||
good is sitting in your hold, the market shows the average price
|
are fixed the moment you dock at a station and don't change while
|
||||||
you paid for it — handy for comparing against the current price
|
you're there — they're only rerolled the next time you dock (after
|
||||||
before selling.
|
a jump). While a good is sitting in your hold, the market shows the
|
||||||
|
average price you paid for it — handy for comparing against the
|
||||||
|
current price before selling.
|
||||||
|
|
||||||
TRAVEL AND DANGEROUS ENCOUNTERS
|
TRAVEL AND DANGEROUS ENCOUNTERS
|
||||||
A jump within the galaxy costs fuel proportional to distance. The
|
A jump within the galaxy costs fuel proportional to distance. The
|
||||||
|
|
@ -1604,6 +1624,15 @@ QUESTS
|
||||||
be active at a time; your choices along the way affect the reward,
|
be active at a time; your choices along the way affect the reward,
|
||||||
your ship's health, and sometimes your cargo.
|
your ship's health, and sometimes your cargo.
|
||||||
|
|
||||||
|
CUSTOM QUESTS: besides the 8 built-in ones, you can add your own
|
||||||
|
without touching the game's code — drop a .yaml file into a
|
||||||
|
"quests" directory next to the executable (or in the directory
|
||||||
|
you run the game from), then restart the program. The file format
|
||||||
|
and detailed comments are in example_quests/lost_satellite.yaml.
|
||||||
|
Files with errors (bad syntax, incomplete translation, a key that
|
||||||
|
collides with an existing quest) are simply skipped with a
|
||||||
|
warning in stderr — they never break the game as a whole.
|
||||||
|
|
||||||
EQUIPMENT AND REPAIRS
|
EQUIPMENT AND REPAIRS
|
||||||
At the shipyard you can refuel, repair the hull, and buy each
|
At the shipyard you can refuel, repair the hull, and buy each
|
||||||
upgrade once: a fuel scoop, a cargo bay expansion, a shield
|
upgrade once: a fuel scoop, a cargo bay expansion, a shield
|
||||||
|
|
@ -1613,6 +1642,8 @@ CONTROLS
|
||||||
At the station (hub):
|
At the station (hub):
|
||||||
1 market 2 shipyard 3 travel 4 quests
|
1 market 2 shipyard 3 travel 4 quests
|
||||||
q save career and quit to menu
|
q save career and quit to menu
|
||||||
|
N start a new career (with confirmation — your current career
|
||||||
|
will be permanently lost)
|
||||||
Market/shipyard/travel/quests:
|
Market/shipyard/travel/quests:
|
||||||
↑↓ / kj select an item
|
↑↓ / kj select an item
|
||||||
b / s buy/sell 1 unit (market)
|
b / s buy/sell 1 unit (market)
|
||||||
|
|
|
||||||
61
third_party/yaml.v3/.github/workflows/go.yaml
vendored
Normal file
61
third_party/yaml.v3/.github/workflows/go.yaml
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
name: Go
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
go:
|
||||||
|
- "1.5"
|
||||||
|
- "1.6"
|
||||||
|
- "1.7"
|
||||||
|
- "1.8"
|
||||||
|
- "1.9"
|
||||||
|
- "1.10"
|
||||||
|
- "1.11"
|
||||||
|
- "1.12"
|
||||||
|
- "1.13"
|
||||||
|
- "1.14"
|
||||||
|
- "1.15"
|
||||||
|
- "1.16.0-beta1"
|
||||||
|
- "tip"
|
||||||
|
env:
|
||||||
|
GOPATH: ${{ github.workspace }}/go
|
||||||
|
steps:
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/go/src/gopkg.in/yaml.v3
|
||||||
|
- name: Set up Go ${{ matrix.go }}
|
||||||
|
if: matrix.go != 'tip'
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
stable: false
|
||||||
|
- name: Set up Go ${{ matrix.go }}
|
||||||
|
if: matrix.go == 'tip'
|
||||||
|
run: |
|
||||||
|
export GOROOT_BOOTSTRAP=`go env GOROOT`
|
||||||
|
export GOROOT=$HOME/gotip
|
||||||
|
mkdir $HOME/gotip
|
||||||
|
cd $HOME/gotip
|
||||||
|
|
||||||
|
curl -s 'https://go.googlesource.com/go/+/refs/heads/master?format=JSON' | awk '/"commit"/{print substr($2,2,40);exit}' >HEAD
|
||||||
|
awk '{printf("gotip-%s",substr($0,0,7))}' <HEAD >VERSION
|
||||||
|
|
||||||
|
curl -s -o go.tar.gz https://go.googlesource.com/go/+archive/`cat HEAD`.tar.gz
|
||||||
|
tar xfz go.tar.gz
|
||||||
|
|
||||||
|
cd src
|
||||||
|
bash make.bash
|
||||||
|
|
||||||
|
echo "GOROOT=$GOROOT" >> $GITHUB_ENV
|
||||||
|
echo "$GOROOT/bin" >> $GITHUB_PATH
|
||||||
|
- run: go version
|
||||||
|
- run: go get -t ./...
|
||||||
|
working-directory: ${{ github.workspace }}/go/src/gopkg.in/yaml.v3
|
||||||
|
- run: go test .
|
||||||
|
working-directory: ${{ github.workspace }}/go/src/gopkg.in/yaml.v3
|
||||||
50
third_party/yaml.v3/LICENSE
vendored
Normal file
50
third_party/yaml.v3/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
|
||||||
|
This project is covered by two different licenses: MIT and Apache.
|
||||||
|
|
||||||
|
#### MIT License ####
|
||||||
|
|
||||||
|
The following files were ported to Go from C files of libyaml, and thus
|
||||||
|
are still covered by their original MIT license, with the additional
|
||||||
|
copyright staring in 2011 when the project was ported over:
|
||||||
|
|
||||||
|
apic.go emitterc.go parserc.go readerc.go scannerc.go
|
||||||
|
writerc.go yamlh.go yamlprivateh.go
|
||||||
|
|
||||||
|
Copyright (c) 2006-2010 Kirill Simonov
|
||||||
|
Copyright (c) 2006-2011 Kirill Simonov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
### Apache License ###
|
||||||
|
|
||||||
|
All the remaining project files are covered by the Apache license:
|
||||||
|
|
||||||
|
Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
150
third_party/yaml.v3/README.md
vendored
Normal file
150
third_party/yaml.v3/README.md
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
# YAML support for the Go language
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
The yaml package enables Go programs to comfortably encode and decode YAML
|
||||||
|
values. It was developed within [Canonical](https://www.canonical.com) as
|
||||||
|
part of the [juju](https://juju.ubuntu.com) project, and is based on a
|
||||||
|
pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML)
|
||||||
|
C library to parse and generate YAML data quickly and reliably.
|
||||||
|
|
||||||
|
Compatibility
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The yaml package supports most of YAML 1.2, but preserves some behavior
|
||||||
|
from 1.1 for backwards compatibility.
|
||||||
|
|
||||||
|
Specifically, as of v3 of the yaml package:
|
||||||
|
|
||||||
|
- YAML 1.1 bools (_yes/no, on/off_) are supported as long as they are being
|
||||||
|
decoded into a typed bool value. Otherwise they behave as a string. Booleans
|
||||||
|
in YAML 1.2 are _true/false_ only.
|
||||||
|
- Octals encode and decode as _0777_ per YAML 1.1, rather than _0o777_
|
||||||
|
as specified in YAML 1.2, because most parsers still use the old format.
|
||||||
|
Octals in the _0o777_ format are supported though, so new files work.
|
||||||
|
- Does not support base-60 floats. These are gone from YAML 1.2, and were
|
||||||
|
actually never supported by this package as it's clearly a poor choice.
|
||||||
|
|
||||||
|
and offers backwards
|
||||||
|
compatibility with YAML 1.1 in some cases.
|
||||||
|
1.2, including support for
|
||||||
|
anchors, tags, map merging, etc. Multi-document unmarshalling is not yet
|
||||||
|
implemented, and base-60 floats from YAML 1.1 are purposefully not
|
||||||
|
supported since they're a poor design and are gone in YAML 1.2.
|
||||||
|
|
||||||
|
Installation and usage
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The import path for the package is *gopkg.in/yaml.v3*.
|
||||||
|
|
||||||
|
To install it, run:
|
||||||
|
|
||||||
|
go get gopkg.in/yaml.v3
|
||||||
|
|
||||||
|
API documentation
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
If opened in a browser, the import path itself leads to the API documentation:
|
||||||
|
|
||||||
|
- [https://gopkg.in/yaml.v3](https://gopkg.in/yaml.v3)
|
||||||
|
|
||||||
|
API stability
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The package API for yaml v3 will remain stable as described in [gopkg.in](https://gopkg.in).
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
The yaml package is licensed under the MIT and Apache License 2.0 licenses.
|
||||||
|
Please see the LICENSE file for details.
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var data = `
|
||||||
|
a: Easy!
|
||||||
|
b:
|
||||||
|
c: 2
|
||||||
|
d: [3, 4]
|
||||||
|
`
|
||||||
|
|
||||||
|
// Note: struct fields must be public in order for unmarshal to
|
||||||
|
// correctly populate the data.
|
||||||
|
type T struct {
|
||||||
|
A string
|
||||||
|
B struct {
|
||||||
|
RenamedC int `yaml:"c"`
|
||||||
|
D []int `yaml:",flow"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
t := T{}
|
||||||
|
|
||||||
|
err := yaml.Unmarshal([]byte(data), &t)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("--- t:\n%v\n\n", t)
|
||||||
|
|
||||||
|
d, err := yaml.Marshal(&t)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("--- t dump:\n%s\n\n", string(d))
|
||||||
|
|
||||||
|
m := make(map[interface{}]interface{})
|
||||||
|
|
||||||
|
err = yaml.Unmarshal([]byte(data), &m)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("--- m:\n%v\n\n", m)
|
||||||
|
|
||||||
|
d, err = yaml.Marshal(&m)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("--- m dump:\n%s\n\n", string(d))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This example will generate the following output:
|
||||||
|
|
||||||
|
```
|
||||||
|
--- t:
|
||||||
|
{Easy! {2 [3 4]}}
|
||||||
|
|
||||||
|
--- t dump:
|
||||||
|
a: Easy!
|
||||||
|
b:
|
||||||
|
c: 2
|
||||||
|
d: [3, 4]
|
||||||
|
|
||||||
|
|
||||||
|
--- m:
|
||||||
|
map[a:Easy! b:map[c:2 d:[3 4]]]
|
||||||
|
|
||||||
|
--- m dump:
|
||||||
|
a: Easy!
|
||||||
|
b:
|
||||||
|
c: 2
|
||||||
|
d:
|
||||||
|
- 3
|
||||||
|
- 4
|
||||||
|
```
|
||||||
|
|
||||||
747
third_party/yaml.v3/apic.go
vendored
Normal file
747
third_party/yaml.v3/apic.go
vendored
Normal file
|
|
@ -0,0 +1,747 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
// Copyright (c) 2006-2010 Kirill Simonov
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
// so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func yaml_insert_token(parser *yaml_parser_t, pos int, token *yaml_token_t) {
|
||||||
|
//fmt.Println("yaml_insert_token", "pos:", pos, "typ:", token.typ, "head:", parser.tokens_head, "len:", len(parser.tokens))
|
||||||
|
|
||||||
|
// Check if we can move the queue at the beginning of the buffer.
|
||||||
|
if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) {
|
||||||
|
if parser.tokens_head != len(parser.tokens) {
|
||||||
|
copy(parser.tokens, parser.tokens[parser.tokens_head:])
|
||||||
|
}
|
||||||
|
parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head]
|
||||||
|
parser.tokens_head = 0
|
||||||
|
}
|
||||||
|
parser.tokens = append(parser.tokens, *token)
|
||||||
|
if pos < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:])
|
||||||
|
parser.tokens[parser.tokens_head+pos] = *token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new parser object.
|
||||||
|
func yaml_parser_initialize(parser *yaml_parser_t) bool {
|
||||||
|
*parser = yaml_parser_t{
|
||||||
|
raw_buffer: make([]byte, 0, input_raw_buffer_size),
|
||||||
|
buffer: make([]byte, 0, input_buffer_size),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy a parser object.
|
||||||
|
func yaml_parser_delete(parser *yaml_parser_t) {
|
||||||
|
*parser = yaml_parser_t{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String read handler.
|
||||||
|
func yaml_string_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) {
|
||||||
|
if parser.input_pos == len(parser.input) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n = copy(buffer, parser.input[parser.input_pos:])
|
||||||
|
parser.input_pos += n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader read handler.
|
||||||
|
func yaml_reader_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) {
|
||||||
|
return parser.input_reader.Read(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a string input.
|
||||||
|
func yaml_parser_set_input_string(parser *yaml_parser_t, input []byte) {
|
||||||
|
if parser.read_handler != nil {
|
||||||
|
panic("must set the input source only once")
|
||||||
|
}
|
||||||
|
parser.read_handler = yaml_string_read_handler
|
||||||
|
parser.input = input
|
||||||
|
parser.input_pos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a file input.
|
||||||
|
func yaml_parser_set_input_reader(parser *yaml_parser_t, r io.Reader) {
|
||||||
|
if parser.read_handler != nil {
|
||||||
|
panic("must set the input source only once")
|
||||||
|
}
|
||||||
|
parser.read_handler = yaml_reader_read_handler
|
||||||
|
parser.input_reader = r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the source encoding.
|
||||||
|
func yaml_parser_set_encoding(parser *yaml_parser_t, encoding yaml_encoding_t) {
|
||||||
|
if parser.encoding != yaml_ANY_ENCODING {
|
||||||
|
panic("must set the encoding only once")
|
||||||
|
}
|
||||||
|
parser.encoding = encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new emitter object.
|
||||||
|
func yaml_emitter_initialize(emitter *yaml_emitter_t) {
|
||||||
|
*emitter = yaml_emitter_t{
|
||||||
|
buffer: make([]byte, output_buffer_size),
|
||||||
|
raw_buffer: make([]byte, 0, output_raw_buffer_size),
|
||||||
|
states: make([]yaml_emitter_state_t, 0, initial_stack_size),
|
||||||
|
events: make([]yaml_event_t, 0, initial_queue_size),
|
||||||
|
best_width: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy an emitter object.
|
||||||
|
func yaml_emitter_delete(emitter *yaml_emitter_t) {
|
||||||
|
*emitter = yaml_emitter_t{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String write handler.
|
||||||
|
func yaml_string_write_handler(emitter *yaml_emitter_t, buffer []byte) error {
|
||||||
|
*emitter.output_buffer = append(*emitter.output_buffer, buffer...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// yaml_writer_write_handler uses emitter.output_writer to write the
|
||||||
|
// emitted text.
|
||||||
|
func yaml_writer_write_handler(emitter *yaml_emitter_t, buffer []byte) error {
|
||||||
|
_, err := emitter.output_writer.Write(buffer)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a string output.
|
||||||
|
func yaml_emitter_set_output_string(emitter *yaml_emitter_t, output_buffer *[]byte) {
|
||||||
|
if emitter.write_handler != nil {
|
||||||
|
panic("must set the output target only once")
|
||||||
|
}
|
||||||
|
emitter.write_handler = yaml_string_write_handler
|
||||||
|
emitter.output_buffer = output_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a file output.
|
||||||
|
func yaml_emitter_set_output_writer(emitter *yaml_emitter_t, w io.Writer) {
|
||||||
|
if emitter.write_handler != nil {
|
||||||
|
panic("must set the output target only once")
|
||||||
|
}
|
||||||
|
emitter.write_handler = yaml_writer_write_handler
|
||||||
|
emitter.output_writer = w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the output encoding.
|
||||||
|
func yaml_emitter_set_encoding(emitter *yaml_emitter_t, encoding yaml_encoding_t) {
|
||||||
|
if emitter.encoding != yaml_ANY_ENCODING {
|
||||||
|
panic("must set the output encoding only once")
|
||||||
|
}
|
||||||
|
emitter.encoding = encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the canonical output style.
|
||||||
|
func yaml_emitter_set_canonical(emitter *yaml_emitter_t, canonical bool) {
|
||||||
|
emitter.canonical = canonical
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the indentation increment.
|
||||||
|
func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) {
|
||||||
|
if indent < 2 || indent > 9 {
|
||||||
|
indent = 2
|
||||||
|
}
|
||||||
|
emitter.best_indent = indent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the preferred line width.
|
||||||
|
func yaml_emitter_set_width(emitter *yaml_emitter_t, width int) {
|
||||||
|
if width < 0 {
|
||||||
|
width = -1
|
||||||
|
}
|
||||||
|
emitter.best_width = width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set if unescaped non-ASCII characters are allowed.
|
||||||
|
func yaml_emitter_set_unicode(emitter *yaml_emitter_t, unicode bool) {
|
||||||
|
emitter.unicode = unicode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the preferred line break character.
|
||||||
|
func yaml_emitter_set_break(emitter *yaml_emitter_t, line_break yaml_break_t) {
|
||||||
|
emitter.line_break = line_break
|
||||||
|
}
|
||||||
|
|
||||||
|
///*
|
||||||
|
// * Destroy a token object.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(void)
|
||||||
|
//yaml_token_delete(yaml_token_t *token)
|
||||||
|
//{
|
||||||
|
// assert(token); // Non-NULL token object expected.
|
||||||
|
//
|
||||||
|
// switch (token.type)
|
||||||
|
// {
|
||||||
|
// case YAML_TAG_DIRECTIVE_TOKEN:
|
||||||
|
// yaml_free(token.data.tag_directive.handle);
|
||||||
|
// yaml_free(token.data.tag_directive.prefix);
|
||||||
|
// break;
|
||||||
|
//
|
||||||
|
// case YAML_ALIAS_TOKEN:
|
||||||
|
// yaml_free(token.data.alias.value);
|
||||||
|
// break;
|
||||||
|
//
|
||||||
|
// case YAML_ANCHOR_TOKEN:
|
||||||
|
// yaml_free(token.data.anchor.value);
|
||||||
|
// break;
|
||||||
|
//
|
||||||
|
// case YAML_TAG_TOKEN:
|
||||||
|
// yaml_free(token.data.tag.handle);
|
||||||
|
// yaml_free(token.data.tag.suffix);
|
||||||
|
// break;
|
||||||
|
//
|
||||||
|
// case YAML_SCALAR_TOKEN:
|
||||||
|
// yaml_free(token.data.scalar.value);
|
||||||
|
// break;
|
||||||
|
//
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// memset(token, 0, sizeof(yaml_token_t));
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///*
|
||||||
|
// * Check if a string is a valid UTF-8 sequence.
|
||||||
|
// *
|
||||||
|
// * Check 'reader.c' for more details on UTF-8 encoding.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//static int
|
||||||
|
//yaml_check_utf8(yaml_char_t *start, size_t length)
|
||||||
|
//{
|
||||||
|
// yaml_char_t *end = start+length;
|
||||||
|
// yaml_char_t *pointer = start;
|
||||||
|
//
|
||||||
|
// while (pointer < end) {
|
||||||
|
// unsigned char octet;
|
||||||
|
// unsigned int width;
|
||||||
|
// unsigned int value;
|
||||||
|
// size_t k;
|
||||||
|
//
|
||||||
|
// octet = pointer[0];
|
||||||
|
// width = (octet & 0x80) == 0x00 ? 1 :
|
||||||
|
// (octet & 0xE0) == 0xC0 ? 2 :
|
||||||
|
// (octet & 0xF0) == 0xE0 ? 3 :
|
||||||
|
// (octet & 0xF8) == 0xF0 ? 4 : 0;
|
||||||
|
// value = (octet & 0x80) == 0x00 ? octet & 0x7F :
|
||||||
|
// (octet & 0xE0) == 0xC0 ? octet & 0x1F :
|
||||||
|
// (octet & 0xF0) == 0xE0 ? octet & 0x0F :
|
||||||
|
// (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0;
|
||||||
|
// if (!width) return 0;
|
||||||
|
// if (pointer+width > end) return 0;
|
||||||
|
// for (k = 1; k < width; k ++) {
|
||||||
|
// octet = pointer[k];
|
||||||
|
// if ((octet & 0xC0) != 0x80) return 0;
|
||||||
|
// value = (value << 6) + (octet & 0x3F);
|
||||||
|
// }
|
||||||
|
// if (!((width == 1) ||
|
||||||
|
// (width == 2 && value >= 0x80) ||
|
||||||
|
// (width == 3 && value >= 0x800) ||
|
||||||
|
// (width == 4 && value >= 0x10000))) return 0;
|
||||||
|
//
|
||||||
|
// pointer += width;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return 1;
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
|
||||||
|
// Create STREAM-START.
|
||||||
|
func yaml_stream_start_event_initialize(event *yaml_event_t, encoding yaml_encoding_t) {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_STREAM_START_EVENT,
|
||||||
|
encoding: encoding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create STREAM-END.
|
||||||
|
func yaml_stream_end_event_initialize(event *yaml_event_t) {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_STREAM_END_EVENT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create DOCUMENT-START.
|
||||||
|
func yaml_document_start_event_initialize(
|
||||||
|
event *yaml_event_t,
|
||||||
|
version_directive *yaml_version_directive_t,
|
||||||
|
tag_directives []yaml_tag_directive_t,
|
||||||
|
implicit bool,
|
||||||
|
) {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_DOCUMENT_START_EVENT,
|
||||||
|
version_directive: version_directive,
|
||||||
|
tag_directives: tag_directives,
|
||||||
|
implicit: implicit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create DOCUMENT-END.
|
||||||
|
func yaml_document_end_event_initialize(event *yaml_event_t, implicit bool) {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_DOCUMENT_END_EVENT,
|
||||||
|
implicit: implicit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ALIAS.
|
||||||
|
func yaml_alias_event_initialize(event *yaml_event_t, anchor []byte) bool {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_ALIAS_EVENT,
|
||||||
|
anchor: anchor,
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SCALAR.
|
||||||
|
func yaml_scalar_event_initialize(event *yaml_event_t, anchor, tag, value []byte, plain_implicit, quoted_implicit bool, style yaml_scalar_style_t) bool {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_SCALAR_EVENT,
|
||||||
|
anchor: anchor,
|
||||||
|
tag: tag,
|
||||||
|
value: value,
|
||||||
|
implicit: plain_implicit,
|
||||||
|
quoted_implicit: quoted_implicit,
|
||||||
|
style: yaml_style_t(style),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SEQUENCE-START.
|
||||||
|
func yaml_sequence_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_sequence_style_t) bool {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_SEQUENCE_START_EVENT,
|
||||||
|
anchor: anchor,
|
||||||
|
tag: tag,
|
||||||
|
implicit: implicit,
|
||||||
|
style: yaml_style_t(style),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SEQUENCE-END.
|
||||||
|
func yaml_sequence_end_event_initialize(event *yaml_event_t) bool {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_SEQUENCE_END_EVENT,
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MAPPING-START.
|
||||||
|
func yaml_mapping_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_mapping_style_t) {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_MAPPING_START_EVENT,
|
||||||
|
anchor: anchor,
|
||||||
|
tag: tag,
|
||||||
|
implicit: implicit,
|
||||||
|
style: yaml_style_t(style),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MAPPING-END.
|
||||||
|
func yaml_mapping_end_event_initialize(event *yaml_event_t) {
|
||||||
|
*event = yaml_event_t{
|
||||||
|
typ: yaml_MAPPING_END_EVENT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy an event object.
|
||||||
|
func yaml_event_delete(event *yaml_event_t) {
|
||||||
|
*event = yaml_event_t{}
|
||||||
|
}
|
||||||
|
|
||||||
|
///*
|
||||||
|
// * Create a document object.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(int)
|
||||||
|
//yaml_document_initialize(document *yaml_document_t,
|
||||||
|
// version_directive *yaml_version_directive_t,
|
||||||
|
// tag_directives_start *yaml_tag_directive_t,
|
||||||
|
// tag_directives_end *yaml_tag_directive_t,
|
||||||
|
// start_implicit int, end_implicit int)
|
||||||
|
//{
|
||||||
|
// struct {
|
||||||
|
// error yaml_error_type_t
|
||||||
|
// } context
|
||||||
|
// struct {
|
||||||
|
// start *yaml_node_t
|
||||||
|
// end *yaml_node_t
|
||||||
|
// top *yaml_node_t
|
||||||
|
// } nodes = { NULL, NULL, NULL }
|
||||||
|
// version_directive_copy *yaml_version_directive_t = NULL
|
||||||
|
// struct {
|
||||||
|
// start *yaml_tag_directive_t
|
||||||
|
// end *yaml_tag_directive_t
|
||||||
|
// top *yaml_tag_directive_t
|
||||||
|
// } tag_directives_copy = { NULL, NULL, NULL }
|
||||||
|
// value yaml_tag_directive_t = { NULL, NULL }
|
||||||
|
// mark yaml_mark_t = { 0, 0, 0 }
|
||||||
|
//
|
||||||
|
// assert(document) // Non-NULL document object is expected.
|
||||||
|
// assert((tag_directives_start && tag_directives_end) ||
|
||||||
|
// (tag_directives_start == tag_directives_end))
|
||||||
|
// // Valid tag directives are expected.
|
||||||
|
//
|
||||||
|
// if (!STACK_INIT(&context, nodes, INITIAL_STACK_SIZE)) goto error
|
||||||
|
//
|
||||||
|
// if (version_directive) {
|
||||||
|
// version_directive_copy = yaml_malloc(sizeof(yaml_version_directive_t))
|
||||||
|
// if (!version_directive_copy) goto error
|
||||||
|
// version_directive_copy.major = version_directive.major
|
||||||
|
// version_directive_copy.minor = version_directive.minor
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (tag_directives_start != tag_directives_end) {
|
||||||
|
// tag_directive *yaml_tag_directive_t
|
||||||
|
// if (!STACK_INIT(&context, tag_directives_copy, INITIAL_STACK_SIZE))
|
||||||
|
// goto error
|
||||||
|
// for (tag_directive = tag_directives_start
|
||||||
|
// tag_directive != tag_directives_end; tag_directive ++) {
|
||||||
|
// assert(tag_directive.handle)
|
||||||
|
// assert(tag_directive.prefix)
|
||||||
|
// if (!yaml_check_utf8(tag_directive.handle,
|
||||||
|
// strlen((char *)tag_directive.handle)))
|
||||||
|
// goto error
|
||||||
|
// if (!yaml_check_utf8(tag_directive.prefix,
|
||||||
|
// strlen((char *)tag_directive.prefix)))
|
||||||
|
// goto error
|
||||||
|
// value.handle = yaml_strdup(tag_directive.handle)
|
||||||
|
// value.prefix = yaml_strdup(tag_directive.prefix)
|
||||||
|
// if (!value.handle || !value.prefix) goto error
|
||||||
|
// if (!PUSH(&context, tag_directives_copy, value))
|
||||||
|
// goto error
|
||||||
|
// value.handle = NULL
|
||||||
|
// value.prefix = NULL
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// DOCUMENT_INIT(*document, nodes.start, nodes.end, version_directive_copy,
|
||||||
|
// tag_directives_copy.start, tag_directives_copy.top,
|
||||||
|
// start_implicit, end_implicit, mark, mark)
|
||||||
|
//
|
||||||
|
// return 1
|
||||||
|
//
|
||||||
|
//error:
|
||||||
|
// STACK_DEL(&context, nodes)
|
||||||
|
// yaml_free(version_directive_copy)
|
||||||
|
// while (!STACK_EMPTY(&context, tag_directives_copy)) {
|
||||||
|
// value yaml_tag_directive_t = POP(&context, tag_directives_copy)
|
||||||
|
// yaml_free(value.handle)
|
||||||
|
// yaml_free(value.prefix)
|
||||||
|
// }
|
||||||
|
// STACK_DEL(&context, tag_directives_copy)
|
||||||
|
// yaml_free(value.handle)
|
||||||
|
// yaml_free(value.prefix)
|
||||||
|
//
|
||||||
|
// return 0
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///*
|
||||||
|
// * Destroy a document object.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(void)
|
||||||
|
//yaml_document_delete(document *yaml_document_t)
|
||||||
|
//{
|
||||||
|
// struct {
|
||||||
|
// error yaml_error_type_t
|
||||||
|
// } context
|
||||||
|
// tag_directive *yaml_tag_directive_t
|
||||||
|
//
|
||||||
|
// context.error = YAML_NO_ERROR // Eliminate a compiler warning.
|
||||||
|
//
|
||||||
|
// assert(document) // Non-NULL document object is expected.
|
||||||
|
//
|
||||||
|
// while (!STACK_EMPTY(&context, document.nodes)) {
|
||||||
|
// node yaml_node_t = POP(&context, document.nodes)
|
||||||
|
// yaml_free(node.tag)
|
||||||
|
// switch (node.type) {
|
||||||
|
// case YAML_SCALAR_NODE:
|
||||||
|
// yaml_free(node.data.scalar.value)
|
||||||
|
// break
|
||||||
|
// case YAML_SEQUENCE_NODE:
|
||||||
|
// STACK_DEL(&context, node.data.sequence.items)
|
||||||
|
// break
|
||||||
|
// case YAML_MAPPING_NODE:
|
||||||
|
// STACK_DEL(&context, node.data.mapping.pairs)
|
||||||
|
// break
|
||||||
|
// default:
|
||||||
|
// assert(0) // Should not happen.
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// STACK_DEL(&context, document.nodes)
|
||||||
|
//
|
||||||
|
// yaml_free(document.version_directive)
|
||||||
|
// for (tag_directive = document.tag_directives.start
|
||||||
|
// tag_directive != document.tag_directives.end
|
||||||
|
// tag_directive++) {
|
||||||
|
// yaml_free(tag_directive.handle)
|
||||||
|
// yaml_free(tag_directive.prefix)
|
||||||
|
// }
|
||||||
|
// yaml_free(document.tag_directives.start)
|
||||||
|
//
|
||||||
|
// memset(document, 0, sizeof(yaml_document_t))
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * Get a document node.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(yaml_node_t *)
|
||||||
|
//yaml_document_get_node(document *yaml_document_t, index int)
|
||||||
|
//{
|
||||||
|
// assert(document) // Non-NULL document object is expected.
|
||||||
|
//
|
||||||
|
// if (index > 0 && document.nodes.start + index <= document.nodes.top) {
|
||||||
|
// return document.nodes.start + index - 1
|
||||||
|
// }
|
||||||
|
// return NULL
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * Get the root object.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(yaml_node_t *)
|
||||||
|
//yaml_document_get_root_node(document *yaml_document_t)
|
||||||
|
//{
|
||||||
|
// assert(document) // Non-NULL document object is expected.
|
||||||
|
//
|
||||||
|
// if (document.nodes.top != document.nodes.start) {
|
||||||
|
// return document.nodes.start
|
||||||
|
// }
|
||||||
|
// return NULL
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///*
|
||||||
|
// * Add a scalar node to a document.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(int)
|
||||||
|
//yaml_document_add_scalar(document *yaml_document_t,
|
||||||
|
// tag *yaml_char_t, value *yaml_char_t, length int,
|
||||||
|
// style yaml_scalar_style_t)
|
||||||
|
//{
|
||||||
|
// struct {
|
||||||
|
// error yaml_error_type_t
|
||||||
|
// } context
|
||||||
|
// mark yaml_mark_t = { 0, 0, 0 }
|
||||||
|
// tag_copy *yaml_char_t = NULL
|
||||||
|
// value_copy *yaml_char_t = NULL
|
||||||
|
// node yaml_node_t
|
||||||
|
//
|
||||||
|
// assert(document) // Non-NULL document object is expected.
|
||||||
|
// assert(value) // Non-NULL value is expected.
|
||||||
|
//
|
||||||
|
// if (!tag) {
|
||||||
|
// tag = (yaml_char_t *)YAML_DEFAULT_SCALAR_TAG
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
|
||||||
|
// tag_copy = yaml_strdup(tag)
|
||||||
|
// if (!tag_copy) goto error
|
||||||
|
//
|
||||||
|
// if (length < 0) {
|
||||||
|
// length = strlen((char *)value)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!yaml_check_utf8(value, length)) goto error
|
||||||
|
// value_copy = yaml_malloc(length+1)
|
||||||
|
// if (!value_copy) goto error
|
||||||
|
// memcpy(value_copy, value, length)
|
||||||
|
// value_copy[length] = '\0'
|
||||||
|
//
|
||||||
|
// SCALAR_NODE_INIT(node, tag_copy, value_copy, length, style, mark, mark)
|
||||||
|
// if (!PUSH(&context, document.nodes, node)) goto error
|
||||||
|
//
|
||||||
|
// return document.nodes.top - document.nodes.start
|
||||||
|
//
|
||||||
|
//error:
|
||||||
|
// yaml_free(tag_copy)
|
||||||
|
// yaml_free(value_copy)
|
||||||
|
//
|
||||||
|
// return 0
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///*
|
||||||
|
// * Add a sequence node to a document.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(int)
|
||||||
|
//yaml_document_add_sequence(document *yaml_document_t,
|
||||||
|
// tag *yaml_char_t, style yaml_sequence_style_t)
|
||||||
|
//{
|
||||||
|
// struct {
|
||||||
|
// error yaml_error_type_t
|
||||||
|
// } context
|
||||||
|
// mark yaml_mark_t = { 0, 0, 0 }
|
||||||
|
// tag_copy *yaml_char_t = NULL
|
||||||
|
// struct {
|
||||||
|
// start *yaml_node_item_t
|
||||||
|
// end *yaml_node_item_t
|
||||||
|
// top *yaml_node_item_t
|
||||||
|
// } items = { NULL, NULL, NULL }
|
||||||
|
// node yaml_node_t
|
||||||
|
//
|
||||||
|
// assert(document) // Non-NULL document object is expected.
|
||||||
|
//
|
||||||
|
// if (!tag) {
|
||||||
|
// tag = (yaml_char_t *)YAML_DEFAULT_SEQUENCE_TAG
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
|
||||||
|
// tag_copy = yaml_strdup(tag)
|
||||||
|
// if (!tag_copy) goto error
|
||||||
|
//
|
||||||
|
// if (!STACK_INIT(&context, items, INITIAL_STACK_SIZE)) goto error
|
||||||
|
//
|
||||||
|
// SEQUENCE_NODE_INIT(node, tag_copy, items.start, items.end,
|
||||||
|
// style, mark, mark)
|
||||||
|
// if (!PUSH(&context, document.nodes, node)) goto error
|
||||||
|
//
|
||||||
|
// return document.nodes.top - document.nodes.start
|
||||||
|
//
|
||||||
|
//error:
|
||||||
|
// STACK_DEL(&context, items)
|
||||||
|
// yaml_free(tag_copy)
|
||||||
|
//
|
||||||
|
// return 0
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///*
|
||||||
|
// * Add a mapping node to a document.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(int)
|
||||||
|
//yaml_document_add_mapping(document *yaml_document_t,
|
||||||
|
// tag *yaml_char_t, style yaml_mapping_style_t)
|
||||||
|
//{
|
||||||
|
// struct {
|
||||||
|
// error yaml_error_type_t
|
||||||
|
// } context
|
||||||
|
// mark yaml_mark_t = { 0, 0, 0 }
|
||||||
|
// tag_copy *yaml_char_t = NULL
|
||||||
|
// struct {
|
||||||
|
// start *yaml_node_pair_t
|
||||||
|
// end *yaml_node_pair_t
|
||||||
|
// top *yaml_node_pair_t
|
||||||
|
// } pairs = { NULL, NULL, NULL }
|
||||||
|
// node yaml_node_t
|
||||||
|
//
|
||||||
|
// assert(document) // Non-NULL document object is expected.
|
||||||
|
//
|
||||||
|
// if (!tag) {
|
||||||
|
// tag = (yaml_char_t *)YAML_DEFAULT_MAPPING_TAG
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
|
||||||
|
// tag_copy = yaml_strdup(tag)
|
||||||
|
// if (!tag_copy) goto error
|
||||||
|
//
|
||||||
|
// if (!STACK_INIT(&context, pairs, INITIAL_STACK_SIZE)) goto error
|
||||||
|
//
|
||||||
|
// MAPPING_NODE_INIT(node, tag_copy, pairs.start, pairs.end,
|
||||||
|
// style, mark, mark)
|
||||||
|
// if (!PUSH(&context, document.nodes, node)) goto error
|
||||||
|
//
|
||||||
|
// return document.nodes.top - document.nodes.start
|
||||||
|
//
|
||||||
|
//error:
|
||||||
|
// STACK_DEL(&context, pairs)
|
||||||
|
// yaml_free(tag_copy)
|
||||||
|
//
|
||||||
|
// return 0
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///*
|
||||||
|
// * Append an item to a sequence node.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(int)
|
||||||
|
//yaml_document_append_sequence_item(document *yaml_document_t,
|
||||||
|
// sequence int, item int)
|
||||||
|
//{
|
||||||
|
// struct {
|
||||||
|
// error yaml_error_type_t
|
||||||
|
// } context
|
||||||
|
//
|
||||||
|
// assert(document) // Non-NULL document is required.
|
||||||
|
// assert(sequence > 0
|
||||||
|
// && document.nodes.start + sequence <= document.nodes.top)
|
||||||
|
// // Valid sequence id is required.
|
||||||
|
// assert(document.nodes.start[sequence-1].type == YAML_SEQUENCE_NODE)
|
||||||
|
// // A sequence node is required.
|
||||||
|
// assert(item > 0 && document.nodes.start + item <= document.nodes.top)
|
||||||
|
// // Valid item id is required.
|
||||||
|
//
|
||||||
|
// if (!PUSH(&context,
|
||||||
|
// document.nodes.start[sequence-1].data.sequence.items, item))
|
||||||
|
// return 0
|
||||||
|
//
|
||||||
|
// return 1
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///*
|
||||||
|
// * Append a pair of a key and a value to a mapping node.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
//YAML_DECLARE(int)
|
||||||
|
//yaml_document_append_mapping_pair(document *yaml_document_t,
|
||||||
|
// mapping int, key int, value int)
|
||||||
|
//{
|
||||||
|
// struct {
|
||||||
|
// error yaml_error_type_t
|
||||||
|
// } context
|
||||||
|
//
|
||||||
|
// pair yaml_node_pair_t
|
||||||
|
//
|
||||||
|
// assert(document) // Non-NULL document is required.
|
||||||
|
// assert(mapping > 0
|
||||||
|
// && document.nodes.start + mapping <= document.nodes.top)
|
||||||
|
// // Valid mapping id is required.
|
||||||
|
// assert(document.nodes.start[mapping-1].type == YAML_MAPPING_NODE)
|
||||||
|
// // A mapping node is required.
|
||||||
|
// assert(key > 0 && document.nodes.start + key <= document.nodes.top)
|
||||||
|
// // Valid key id is required.
|
||||||
|
// assert(value > 0 && document.nodes.start + value <= document.nodes.top)
|
||||||
|
// // Valid value id is required.
|
||||||
|
//
|
||||||
|
// pair.key = key
|
||||||
|
// pair.value = value
|
||||||
|
//
|
||||||
|
// if (!PUSH(&context,
|
||||||
|
// document.nodes.start[mapping-1].data.mapping.pairs, pair))
|
||||||
|
// return 0
|
||||||
|
//
|
||||||
|
// return 1
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//
|
||||||
1000
third_party/yaml.v3/decode.go
vendored
Normal file
1000
third_party/yaml.v3/decode.go
vendored
Normal file
File diff suppressed because it is too large
Load diff
2019
third_party/yaml.v3/emitterc.go
vendored
Normal file
2019
third_party/yaml.v3/emitterc.go
vendored
Normal file
File diff suppressed because it is too large
Load diff
577
third_party/yaml.v3/encode.go
vendored
Normal file
577
third_party/yaml.v3/encode.go
vendored
Normal file
|
|
@ -0,0 +1,577 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
emitter yaml_emitter_t
|
||||||
|
event yaml_event_t
|
||||||
|
out []byte
|
||||||
|
flow bool
|
||||||
|
indent int
|
||||||
|
doneInit bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEncoder() *encoder {
|
||||||
|
e := &encoder{}
|
||||||
|
yaml_emitter_initialize(&e.emitter)
|
||||||
|
yaml_emitter_set_output_string(&e.emitter, &e.out)
|
||||||
|
yaml_emitter_set_unicode(&e.emitter, true)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEncoderWithWriter(w io.Writer) *encoder {
|
||||||
|
e := &encoder{}
|
||||||
|
yaml_emitter_initialize(&e.emitter)
|
||||||
|
yaml_emitter_set_output_writer(&e.emitter, w)
|
||||||
|
yaml_emitter_set_unicode(&e.emitter, true)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) init() {
|
||||||
|
if e.doneInit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e.indent == 0 {
|
||||||
|
e.indent = 4
|
||||||
|
}
|
||||||
|
e.emitter.best_indent = e.indent
|
||||||
|
yaml_stream_start_event_initialize(&e.event, yaml_UTF8_ENCODING)
|
||||||
|
e.emit()
|
||||||
|
e.doneInit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) finish() {
|
||||||
|
e.emitter.open_ended = false
|
||||||
|
yaml_stream_end_event_initialize(&e.event)
|
||||||
|
e.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) destroy() {
|
||||||
|
yaml_emitter_delete(&e.emitter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) emit() {
|
||||||
|
// This will internally delete the e.event value.
|
||||||
|
e.must(yaml_emitter_emit(&e.emitter, &e.event))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) must(ok bool) {
|
||||||
|
if !ok {
|
||||||
|
msg := e.emitter.problem
|
||||||
|
if msg == "" {
|
||||||
|
msg = "unknown problem generating YAML content"
|
||||||
|
}
|
||||||
|
failf("%s", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) marshalDoc(tag string, in reflect.Value) {
|
||||||
|
e.init()
|
||||||
|
var node *Node
|
||||||
|
if in.IsValid() {
|
||||||
|
node, _ = in.Interface().(*Node)
|
||||||
|
}
|
||||||
|
if node != nil && node.Kind == DocumentNode {
|
||||||
|
e.nodev(in)
|
||||||
|
} else {
|
||||||
|
yaml_document_start_event_initialize(&e.event, nil, nil, true)
|
||||||
|
e.emit()
|
||||||
|
e.marshal(tag, in)
|
||||||
|
yaml_document_end_event_initialize(&e.event, true)
|
||||||
|
e.emit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) marshal(tag string, in reflect.Value) {
|
||||||
|
tag = shortTag(tag)
|
||||||
|
if !in.IsValid() || in.Kind() == reflect.Ptr && in.IsNil() {
|
||||||
|
e.nilv()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
iface := in.Interface()
|
||||||
|
switch value := iface.(type) {
|
||||||
|
case *Node:
|
||||||
|
e.nodev(in)
|
||||||
|
return
|
||||||
|
case Node:
|
||||||
|
if !in.CanAddr() {
|
||||||
|
var n = reflect.New(in.Type()).Elem()
|
||||||
|
n.Set(in)
|
||||||
|
in = n
|
||||||
|
}
|
||||||
|
e.nodev(in.Addr())
|
||||||
|
return
|
||||||
|
case time.Time:
|
||||||
|
e.timev(tag, in)
|
||||||
|
return
|
||||||
|
case *time.Time:
|
||||||
|
e.timev(tag, in.Elem())
|
||||||
|
return
|
||||||
|
case time.Duration:
|
||||||
|
e.stringv(tag, reflect.ValueOf(value.String()))
|
||||||
|
return
|
||||||
|
case Marshaler:
|
||||||
|
v, err := value.MarshalYAML()
|
||||||
|
if err != nil {
|
||||||
|
fail(err)
|
||||||
|
}
|
||||||
|
if v == nil {
|
||||||
|
e.nilv()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.marshal(tag, reflect.ValueOf(v))
|
||||||
|
return
|
||||||
|
case encoding.TextMarshaler:
|
||||||
|
text, err := value.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
fail(err)
|
||||||
|
}
|
||||||
|
in = reflect.ValueOf(string(text))
|
||||||
|
case nil:
|
||||||
|
e.nilv()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch in.Kind() {
|
||||||
|
case reflect.Interface:
|
||||||
|
e.marshal(tag, in.Elem())
|
||||||
|
case reflect.Map:
|
||||||
|
e.mapv(tag, in)
|
||||||
|
case reflect.Ptr:
|
||||||
|
e.marshal(tag, in.Elem())
|
||||||
|
case reflect.Struct:
|
||||||
|
e.structv(tag, in)
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
e.slicev(tag, in)
|
||||||
|
case reflect.String:
|
||||||
|
e.stringv(tag, in)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
e.intv(tag, in)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
e.uintv(tag, in)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
e.floatv(tag, in)
|
||||||
|
case reflect.Bool:
|
||||||
|
e.boolv(tag, in)
|
||||||
|
default:
|
||||||
|
panic("cannot marshal type: " + in.Type().String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) mapv(tag string, in reflect.Value) {
|
||||||
|
e.mappingv(tag, func() {
|
||||||
|
keys := keyList(in.MapKeys())
|
||||||
|
sort.Sort(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
e.marshal("", k)
|
||||||
|
e.marshal("", in.MapIndex(k))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) fieldByIndex(v reflect.Value, index []int) (field reflect.Value) {
|
||||||
|
for _, num := range index {
|
||||||
|
for {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
return reflect.Value{}
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
v = v.Field(num)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) structv(tag string, in reflect.Value) {
|
||||||
|
sinfo, err := getStructInfo(in.Type())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
e.mappingv(tag, func() {
|
||||||
|
for _, info := range sinfo.FieldsList {
|
||||||
|
var value reflect.Value
|
||||||
|
if info.Inline == nil {
|
||||||
|
value = in.Field(info.Num)
|
||||||
|
} else {
|
||||||
|
value = e.fieldByIndex(in, info.Inline)
|
||||||
|
if !value.IsValid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.OmitEmpty && isZero(value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.marshal("", reflect.ValueOf(info.Key))
|
||||||
|
e.flow = info.Flow
|
||||||
|
e.marshal("", value)
|
||||||
|
}
|
||||||
|
if sinfo.InlineMap >= 0 {
|
||||||
|
m := in.Field(sinfo.InlineMap)
|
||||||
|
if m.Len() > 0 {
|
||||||
|
e.flow = false
|
||||||
|
keys := keyList(m.MapKeys())
|
||||||
|
sort.Sort(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
if _, found := sinfo.FieldsMap[k.String()]; found {
|
||||||
|
panic(fmt.Sprintf("cannot have key %q in inlined map: conflicts with struct field", k.String()))
|
||||||
|
}
|
||||||
|
e.marshal("", k)
|
||||||
|
e.flow = false
|
||||||
|
e.marshal("", m.MapIndex(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) mappingv(tag string, f func()) {
|
||||||
|
implicit := tag == ""
|
||||||
|
style := yaml_BLOCK_MAPPING_STYLE
|
||||||
|
if e.flow {
|
||||||
|
e.flow = false
|
||||||
|
style = yaml_FLOW_MAPPING_STYLE
|
||||||
|
}
|
||||||
|
yaml_mapping_start_event_initialize(&e.event, nil, []byte(tag), implicit, style)
|
||||||
|
e.emit()
|
||||||
|
f()
|
||||||
|
yaml_mapping_end_event_initialize(&e.event)
|
||||||
|
e.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) slicev(tag string, in reflect.Value) {
|
||||||
|
implicit := tag == ""
|
||||||
|
style := yaml_BLOCK_SEQUENCE_STYLE
|
||||||
|
if e.flow {
|
||||||
|
e.flow = false
|
||||||
|
style = yaml_FLOW_SEQUENCE_STYLE
|
||||||
|
}
|
||||||
|
e.must(yaml_sequence_start_event_initialize(&e.event, nil, []byte(tag), implicit, style))
|
||||||
|
e.emit()
|
||||||
|
n := in.Len()
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
e.marshal("", in.Index(i))
|
||||||
|
}
|
||||||
|
e.must(yaml_sequence_end_event_initialize(&e.event))
|
||||||
|
e.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBase60 returns whether s is in base 60 notation as defined in YAML 1.1.
|
||||||
|
//
|
||||||
|
// The base 60 float notation in YAML 1.1 is a terrible idea and is unsupported
|
||||||
|
// in YAML 1.2 and by this package, but these should be marshalled quoted for
|
||||||
|
// the time being for compatibility with other parsers.
|
||||||
|
func isBase60Float(s string) (result bool) {
|
||||||
|
// Fast path.
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c := s[0]
|
||||||
|
if !(c == '+' || c == '-' || c >= '0' && c <= '9') || strings.IndexByte(s, ':') < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Do the full match.
|
||||||
|
return base60float.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From http://yaml.org/type/float.html, except the regular expression there
|
||||||
|
// is bogus. In practice parsers do not enforce the "\.[0-9_]*" suffix.
|
||||||
|
var base60float = regexp.MustCompile(`^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$`)
|
||||||
|
|
||||||
|
// isOldBool returns whether s is bool notation as defined in YAML 1.1.
|
||||||
|
//
|
||||||
|
// We continue to force strings that YAML 1.1 would interpret as booleans to be
|
||||||
|
// rendered as quotes strings so that the marshalled output valid for YAML 1.1
|
||||||
|
// parsing.
|
||||||
|
func isOldBool(s string) (result bool) {
|
||||||
|
switch s {
|
||||||
|
case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON",
|
||||||
|
"n", "N", "no", "No", "NO", "off", "Off", "OFF":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) stringv(tag string, in reflect.Value) {
|
||||||
|
var style yaml_scalar_style_t
|
||||||
|
s := in.String()
|
||||||
|
canUsePlain := true
|
||||||
|
switch {
|
||||||
|
case !utf8.ValidString(s):
|
||||||
|
if tag == binaryTag {
|
||||||
|
failf("explicitly tagged !!binary data must be base64-encoded")
|
||||||
|
}
|
||||||
|
if tag != "" {
|
||||||
|
failf("cannot marshal invalid UTF-8 data as %s", shortTag(tag))
|
||||||
|
}
|
||||||
|
// It can't be encoded directly as YAML so use a binary tag
|
||||||
|
// and encode it as base64.
|
||||||
|
tag = binaryTag
|
||||||
|
s = encodeBase64(s)
|
||||||
|
case tag == "":
|
||||||
|
// Check to see if it would resolve to a specific
|
||||||
|
// tag when encoded unquoted. If it doesn't,
|
||||||
|
// there's no need to quote it.
|
||||||
|
rtag, _ := resolve("", s)
|
||||||
|
canUsePlain = rtag == strTag && !(isBase60Float(s) || isOldBool(s))
|
||||||
|
}
|
||||||
|
// Note: it's possible for user code to emit invalid YAML
|
||||||
|
// if they explicitly specify a tag and a string containing
|
||||||
|
// text that's incompatible with that tag.
|
||||||
|
switch {
|
||||||
|
case strings.Contains(s, "\n"):
|
||||||
|
if e.flow {
|
||||||
|
style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
|
||||||
|
} else {
|
||||||
|
style = yaml_LITERAL_SCALAR_STYLE
|
||||||
|
}
|
||||||
|
case canUsePlain:
|
||||||
|
style = yaml_PLAIN_SCALAR_STYLE
|
||||||
|
default:
|
||||||
|
style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
|
||||||
|
}
|
||||||
|
e.emitScalar(s, "", tag, style, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) boolv(tag string, in reflect.Value) {
|
||||||
|
var s string
|
||||||
|
if in.Bool() {
|
||||||
|
s = "true"
|
||||||
|
} else {
|
||||||
|
s = "false"
|
||||||
|
}
|
||||||
|
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) intv(tag string, in reflect.Value) {
|
||||||
|
s := strconv.FormatInt(in.Int(), 10)
|
||||||
|
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) uintv(tag string, in reflect.Value) {
|
||||||
|
s := strconv.FormatUint(in.Uint(), 10)
|
||||||
|
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) timev(tag string, in reflect.Value) {
|
||||||
|
t := in.Interface().(time.Time)
|
||||||
|
s := t.Format(time.RFC3339Nano)
|
||||||
|
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) floatv(tag string, in reflect.Value) {
|
||||||
|
// Issue #352: When formatting, use the precision of the underlying value
|
||||||
|
precision := 64
|
||||||
|
if in.Kind() == reflect.Float32 {
|
||||||
|
precision = 32
|
||||||
|
}
|
||||||
|
|
||||||
|
s := strconv.FormatFloat(in.Float(), 'g', -1, precision)
|
||||||
|
switch s {
|
||||||
|
case "+Inf":
|
||||||
|
s = ".inf"
|
||||||
|
case "-Inf":
|
||||||
|
s = "-.inf"
|
||||||
|
case "NaN":
|
||||||
|
s = ".nan"
|
||||||
|
}
|
||||||
|
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) nilv() {
|
||||||
|
e.emitScalar("null", "", "", yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) emitScalar(value, anchor, tag string, style yaml_scalar_style_t, head, line, foot, tail []byte) {
|
||||||
|
// TODO Kill this function. Replace all initialize calls by their underlining Go literals.
|
||||||
|
implicit := tag == ""
|
||||||
|
if !implicit {
|
||||||
|
tag = longTag(tag)
|
||||||
|
}
|
||||||
|
e.must(yaml_scalar_event_initialize(&e.event, []byte(anchor), []byte(tag), []byte(value), implicit, implicit, style))
|
||||||
|
e.event.head_comment = head
|
||||||
|
e.event.line_comment = line
|
||||||
|
e.event.foot_comment = foot
|
||||||
|
e.event.tail_comment = tail
|
||||||
|
e.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) nodev(in reflect.Value) {
|
||||||
|
e.node(in.Interface().(*Node), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) node(node *Node, tail string) {
|
||||||
|
// Zero nodes behave as nil.
|
||||||
|
if node.Kind == 0 && node.IsZero() {
|
||||||
|
e.nilv()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the tag was not explicitly requested, and dropping it won't change the
|
||||||
|
// implicit tag of the value, don't include it in the presentation.
|
||||||
|
var tag = node.Tag
|
||||||
|
var stag = shortTag(tag)
|
||||||
|
var forceQuoting bool
|
||||||
|
if tag != "" && node.Style&TaggedStyle == 0 {
|
||||||
|
if node.Kind == ScalarNode {
|
||||||
|
if stag == strTag && node.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 {
|
||||||
|
tag = ""
|
||||||
|
} else {
|
||||||
|
rtag, _ := resolve("", node.Value)
|
||||||
|
if rtag == stag {
|
||||||
|
tag = ""
|
||||||
|
} else if stag == strTag {
|
||||||
|
tag = ""
|
||||||
|
forceQuoting = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var rtag string
|
||||||
|
switch node.Kind {
|
||||||
|
case MappingNode:
|
||||||
|
rtag = mapTag
|
||||||
|
case SequenceNode:
|
||||||
|
rtag = seqTag
|
||||||
|
}
|
||||||
|
if rtag == stag {
|
||||||
|
tag = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node.Kind {
|
||||||
|
case DocumentNode:
|
||||||
|
yaml_document_start_event_initialize(&e.event, nil, nil, true)
|
||||||
|
e.event.head_comment = []byte(node.HeadComment)
|
||||||
|
e.emit()
|
||||||
|
for _, node := range node.Content {
|
||||||
|
e.node(node, "")
|
||||||
|
}
|
||||||
|
yaml_document_end_event_initialize(&e.event, true)
|
||||||
|
e.event.foot_comment = []byte(node.FootComment)
|
||||||
|
e.emit()
|
||||||
|
|
||||||
|
case SequenceNode:
|
||||||
|
style := yaml_BLOCK_SEQUENCE_STYLE
|
||||||
|
if node.Style&FlowStyle != 0 {
|
||||||
|
style = yaml_FLOW_SEQUENCE_STYLE
|
||||||
|
}
|
||||||
|
e.must(yaml_sequence_start_event_initialize(&e.event, []byte(node.Anchor), []byte(longTag(tag)), tag == "", style))
|
||||||
|
e.event.head_comment = []byte(node.HeadComment)
|
||||||
|
e.emit()
|
||||||
|
for _, node := range node.Content {
|
||||||
|
e.node(node, "")
|
||||||
|
}
|
||||||
|
e.must(yaml_sequence_end_event_initialize(&e.event))
|
||||||
|
e.event.line_comment = []byte(node.LineComment)
|
||||||
|
e.event.foot_comment = []byte(node.FootComment)
|
||||||
|
e.emit()
|
||||||
|
|
||||||
|
case MappingNode:
|
||||||
|
style := yaml_BLOCK_MAPPING_STYLE
|
||||||
|
if node.Style&FlowStyle != 0 {
|
||||||
|
style = yaml_FLOW_MAPPING_STYLE
|
||||||
|
}
|
||||||
|
yaml_mapping_start_event_initialize(&e.event, []byte(node.Anchor), []byte(longTag(tag)), tag == "", style)
|
||||||
|
e.event.tail_comment = []byte(tail)
|
||||||
|
e.event.head_comment = []byte(node.HeadComment)
|
||||||
|
e.emit()
|
||||||
|
|
||||||
|
// The tail logic below moves the foot comment of prior keys to the following key,
|
||||||
|
// since the value for each key may be a nested structure and the foot needs to be
|
||||||
|
// processed only the entirety of the value is streamed. The last tail is processed
|
||||||
|
// with the mapping end event.
|
||||||
|
var tail string
|
||||||
|
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||||
|
k := node.Content[i]
|
||||||
|
foot := k.FootComment
|
||||||
|
if foot != "" {
|
||||||
|
kopy := *k
|
||||||
|
kopy.FootComment = ""
|
||||||
|
k = &kopy
|
||||||
|
}
|
||||||
|
e.node(k, tail)
|
||||||
|
tail = foot
|
||||||
|
|
||||||
|
v := node.Content[i+1]
|
||||||
|
e.node(v, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
yaml_mapping_end_event_initialize(&e.event)
|
||||||
|
e.event.tail_comment = []byte(tail)
|
||||||
|
e.event.line_comment = []byte(node.LineComment)
|
||||||
|
e.event.foot_comment = []byte(node.FootComment)
|
||||||
|
e.emit()
|
||||||
|
|
||||||
|
case AliasNode:
|
||||||
|
yaml_alias_event_initialize(&e.event, []byte(node.Value))
|
||||||
|
e.event.head_comment = []byte(node.HeadComment)
|
||||||
|
e.event.line_comment = []byte(node.LineComment)
|
||||||
|
e.event.foot_comment = []byte(node.FootComment)
|
||||||
|
e.emit()
|
||||||
|
|
||||||
|
case ScalarNode:
|
||||||
|
value := node.Value
|
||||||
|
if !utf8.ValidString(value) {
|
||||||
|
if stag == binaryTag {
|
||||||
|
failf("explicitly tagged !!binary data must be base64-encoded")
|
||||||
|
}
|
||||||
|
if stag != "" {
|
||||||
|
failf("cannot marshal invalid UTF-8 data as %s", stag)
|
||||||
|
}
|
||||||
|
// It can't be encoded directly as YAML so use a binary tag
|
||||||
|
// and encode it as base64.
|
||||||
|
tag = binaryTag
|
||||||
|
value = encodeBase64(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
style := yaml_PLAIN_SCALAR_STYLE
|
||||||
|
switch {
|
||||||
|
case node.Style&DoubleQuotedStyle != 0:
|
||||||
|
style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
|
||||||
|
case node.Style&SingleQuotedStyle != 0:
|
||||||
|
style = yaml_SINGLE_QUOTED_SCALAR_STYLE
|
||||||
|
case node.Style&LiteralStyle != 0:
|
||||||
|
style = yaml_LITERAL_SCALAR_STYLE
|
||||||
|
case node.Style&FoldedStyle != 0:
|
||||||
|
style = yaml_FOLDED_SCALAR_STYLE
|
||||||
|
case strings.Contains(value, "\n"):
|
||||||
|
style = yaml_LITERAL_SCALAR_STYLE
|
||||||
|
case forceQuoting:
|
||||||
|
style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
|
||||||
|
}
|
||||||
|
|
||||||
|
e.emitScalar(value, node.Anchor, tag, style, []byte(node.HeadComment), []byte(node.LineComment), []byte(node.FootComment), []byte(tail))
|
||||||
|
default:
|
||||||
|
failf("cannot encode node with unknown kind %d", node.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
third_party/yaml.v3/go.mod
vendored
Normal file
3
third_party/yaml.v3/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module gopkg.in/yaml.v3
|
||||||
|
|
||||||
|
go 1.22.2
|
||||||
1274
third_party/yaml.v3/parserc.go
vendored
Normal file
1274
third_party/yaml.v3/parserc.go
vendored
Normal file
File diff suppressed because it is too large
Load diff
434
third_party/yaml.v3/readerc.go
vendored
Normal file
434
third_party/yaml.v3/readerc.go
vendored
Normal file
|
|
@ -0,0 +1,434 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
// Copyright (c) 2006-2010 Kirill Simonov
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
// so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set the reader error and return 0.
|
||||||
|
func yaml_parser_set_reader_error(parser *yaml_parser_t, problem string, offset int, value int) bool {
|
||||||
|
parser.error = yaml_READER_ERROR
|
||||||
|
parser.problem = problem
|
||||||
|
parser.problem_offset = offset
|
||||||
|
parser.problem_value = value
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte order marks.
|
||||||
|
const (
|
||||||
|
bom_UTF8 = "\xef\xbb\xbf"
|
||||||
|
bom_UTF16LE = "\xff\xfe"
|
||||||
|
bom_UTF16BE = "\xfe\xff"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Determine the input stream encoding by checking the BOM symbol. If no BOM is
|
||||||
|
// found, the UTF-8 encoding is assumed. Return 1 on success, 0 on failure.
|
||||||
|
func yaml_parser_determine_encoding(parser *yaml_parser_t) bool {
|
||||||
|
// Ensure that we had enough bytes in the raw buffer.
|
||||||
|
for !parser.eof && len(parser.raw_buffer)-parser.raw_buffer_pos < 3 {
|
||||||
|
if !yaml_parser_update_raw_buffer(parser) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the encoding.
|
||||||
|
buf := parser.raw_buffer
|
||||||
|
pos := parser.raw_buffer_pos
|
||||||
|
avail := len(buf) - pos
|
||||||
|
if avail >= 2 && buf[pos] == bom_UTF16LE[0] && buf[pos+1] == bom_UTF16LE[1] {
|
||||||
|
parser.encoding = yaml_UTF16LE_ENCODING
|
||||||
|
parser.raw_buffer_pos += 2
|
||||||
|
parser.offset += 2
|
||||||
|
} else if avail >= 2 && buf[pos] == bom_UTF16BE[0] && buf[pos+1] == bom_UTF16BE[1] {
|
||||||
|
parser.encoding = yaml_UTF16BE_ENCODING
|
||||||
|
parser.raw_buffer_pos += 2
|
||||||
|
parser.offset += 2
|
||||||
|
} else if avail >= 3 && buf[pos] == bom_UTF8[0] && buf[pos+1] == bom_UTF8[1] && buf[pos+2] == bom_UTF8[2] {
|
||||||
|
parser.encoding = yaml_UTF8_ENCODING
|
||||||
|
parser.raw_buffer_pos += 3
|
||||||
|
parser.offset += 3
|
||||||
|
} else {
|
||||||
|
parser.encoding = yaml_UTF8_ENCODING
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the raw buffer.
|
||||||
|
func yaml_parser_update_raw_buffer(parser *yaml_parser_t) bool {
|
||||||
|
size_read := 0
|
||||||
|
|
||||||
|
// Return if the raw buffer is full.
|
||||||
|
if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return on EOF.
|
||||||
|
if parser.eof {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the remaining bytes in the raw buffer to the beginning.
|
||||||
|
if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) {
|
||||||
|
copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:])
|
||||||
|
}
|
||||||
|
parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos]
|
||||||
|
parser.raw_buffer_pos = 0
|
||||||
|
|
||||||
|
// Call the read handler to fill the buffer.
|
||||||
|
size_read, err := parser.read_handler(parser, parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)])
|
||||||
|
parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read]
|
||||||
|
if err == io.EOF {
|
||||||
|
parser.eof = true
|
||||||
|
} else if err != nil {
|
||||||
|
return yaml_parser_set_reader_error(parser, "input error: "+err.Error(), parser.offset, -1)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the buffer contains at least `length` characters.
|
||||||
|
// Return true on success, false on failure.
|
||||||
|
//
|
||||||
|
// The length is supposed to be significantly less that the buffer size.
|
||||||
|
func yaml_parser_update_buffer(parser *yaml_parser_t, length int) bool {
|
||||||
|
if parser.read_handler == nil {
|
||||||
|
panic("read handler must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Go] This function was changed to guarantee the requested length size at EOF.
|
||||||
|
// The fact we need to do this is pretty awful, but the description above implies
|
||||||
|
// for that to be the case, and there are tests
|
||||||
|
|
||||||
|
// If the EOF flag is set and the raw buffer is empty, do nothing.
|
||||||
|
if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) {
|
||||||
|
// [Go] ACTUALLY! Read the documentation of this function above.
|
||||||
|
// This is just broken. To return true, we need to have the
|
||||||
|
// given length in the buffer. Not doing that means every single
|
||||||
|
// check that calls this function to make sure the buffer has a
|
||||||
|
// given length is Go) panicking; or C) accessing invalid memory.
|
||||||
|
//return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if the buffer contains enough characters.
|
||||||
|
if parser.unread >= length {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the input encoding if it is not known yet.
|
||||||
|
if parser.encoding == yaml_ANY_ENCODING {
|
||||||
|
if !yaml_parser_determine_encoding(parser) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the unread characters to the beginning of the buffer.
|
||||||
|
buffer_len := len(parser.buffer)
|
||||||
|
if parser.buffer_pos > 0 && parser.buffer_pos < buffer_len {
|
||||||
|
copy(parser.buffer, parser.buffer[parser.buffer_pos:])
|
||||||
|
buffer_len -= parser.buffer_pos
|
||||||
|
parser.buffer_pos = 0
|
||||||
|
} else if parser.buffer_pos == buffer_len {
|
||||||
|
buffer_len = 0
|
||||||
|
parser.buffer_pos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the whole buffer for writing, and cut it before returning.
|
||||||
|
parser.buffer = parser.buffer[:cap(parser.buffer)]
|
||||||
|
|
||||||
|
// Fill the buffer until it has enough characters.
|
||||||
|
first := true
|
||||||
|
for parser.unread < length {
|
||||||
|
|
||||||
|
// Fill the raw buffer if necessary.
|
||||||
|
if !first || parser.raw_buffer_pos == len(parser.raw_buffer) {
|
||||||
|
if !yaml_parser_update_raw_buffer(parser) {
|
||||||
|
parser.buffer = parser.buffer[:buffer_len]
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
// Decode the raw buffer.
|
||||||
|
inner:
|
||||||
|
for parser.raw_buffer_pos != len(parser.raw_buffer) {
|
||||||
|
var value rune
|
||||||
|
var width int
|
||||||
|
|
||||||
|
raw_unread := len(parser.raw_buffer) - parser.raw_buffer_pos
|
||||||
|
|
||||||
|
// Decode the next character.
|
||||||
|
switch parser.encoding {
|
||||||
|
case yaml_UTF8_ENCODING:
|
||||||
|
// Decode a UTF-8 character. Check RFC 3629
|
||||||
|
// (http://www.ietf.org/rfc/rfc3629.txt) for more details.
|
||||||
|
//
|
||||||
|
// The following table (taken from the RFC) is used for
|
||||||
|
// decoding.
|
||||||
|
//
|
||||||
|
// Char. number range | UTF-8 octet sequence
|
||||||
|
// (hexadecimal) | (binary)
|
||||||
|
// --------------------+------------------------------------
|
||||||
|
// 0000 0000-0000 007F | 0xxxxxxx
|
||||||
|
// 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
|
||||||
|
// 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
|
||||||
|
// 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
||||||
|
//
|
||||||
|
// Additionally, the characters in the range 0xD800-0xDFFF
|
||||||
|
// are prohibited as they are reserved for use with UTF-16
|
||||||
|
// surrogate pairs.
|
||||||
|
|
||||||
|
// Determine the length of the UTF-8 sequence.
|
||||||
|
octet := parser.raw_buffer[parser.raw_buffer_pos]
|
||||||
|
switch {
|
||||||
|
case octet&0x80 == 0x00:
|
||||||
|
width = 1
|
||||||
|
case octet&0xE0 == 0xC0:
|
||||||
|
width = 2
|
||||||
|
case octet&0xF0 == 0xE0:
|
||||||
|
width = 3
|
||||||
|
case octet&0xF8 == 0xF0:
|
||||||
|
width = 4
|
||||||
|
default:
|
||||||
|
// The leading octet is invalid.
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"invalid leading UTF-8 octet",
|
||||||
|
parser.offset, int(octet))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the raw buffer contains an incomplete character.
|
||||||
|
if width > raw_unread {
|
||||||
|
if parser.eof {
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"incomplete UTF-8 octet sequence",
|
||||||
|
parser.offset, -1)
|
||||||
|
}
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the leading octet.
|
||||||
|
switch {
|
||||||
|
case octet&0x80 == 0x00:
|
||||||
|
value = rune(octet & 0x7F)
|
||||||
|
case octet&0xE0 == 0xC0:
|
||||||
|
value = rune(octet & 0x1F)
|
||||||
|
case octet&0xF0 == 0xE0:
|
||||||
|
value = rune(octet & 0x0F)
|
||||||
|
case octet&0xF8 == 0xF0:
|
||||||
|
value = rune(octet & 0x07)
|
||||||
|
default:
|
||||||
|
value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and decode the trailing octets.
|
||||||
|
for k := 1; k < width; k++ {
|
||||||
|
octet = parser.raw_buffer[parser.raw_buffer_pos+k]
|
||||||
|
|
||||||
|
// Check if the octet is valid.
|
||||||
|
if (octet & 0xC0) != 0x80 {
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"invalid trailing UTF-8 octet",
|
||||||
|
parser.offset+k, int(octet))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the octet.
|
||||||
|
value = (value << 6) + rune(octet&0x3F)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the length of the sequence against the value.
|
||||||
|
switch {
|
||||||
|
case width == 1:
|
||||||
|
case width == 2 && value >= 0x80:
|
||||||
|
case width == 3 && value >= 0x800:
|
||||||
|
case width == 4 && value >= 0x10000:
|
||||||
|
default:
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"invalid length of a UTF-8 sequence",
|
||||||
|
parser.offset, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the range of the value.
|
||||||
|
if value >= 0xD800 && value <= 0xDFFF || value > 0x10FFFF {
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"invalid Unicode character",
|
||||||
|
parser.offset, int(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
case yaml_UTF16LE_ENCODING, yaml_UTF16BE_ENCODING:
|
||||||
|
var low, high int
|
||||||
|
if parser.encoding == yaml_UTF16LE_ENCODING {
|
||||||
|
low, high = 0, 1
|
||||||
|
} else {
|
||||||
|
low, high = 1, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// The UTF-16 encoding is not as simple as one might
|
||||||
|
// naively think. Check RFC 2781
|
||||||
|
// (http://www.ietf.org/rfc/rfc2781.txt).
|
||||||
|
//
|
||||||
|
// Normally, two subsequent bytes describe a Unicode
|
||||||
|
// character. However a special technique (called a
|
||||||
|
// surrogate pair) is used for specifying character
|
||||||
|
// values larger than 0xFFFF.
|
||||||
|
//
|
||||||
|
// A surrogate pair consists of two pseudo-characters:
|
||||||
|
// high surrogate area (0xD800-0xDBFF)
|
||||||
|
// low surrogate area (0xDC00-0xDFFF)
|
||||||
|
//
|
||||||
|
// The following formulas are used for decoding
|
||||||
|
// and encoding characters using surrogate pairs:
|
||||||
|
//
|
||||||
|
// U = U' + 0x10000 (0x01 00 00 <= U <= 0x10 FF FF)
|
||||||
|
// U' = yyyyyyyyyyxxxxxxxxxx (0 <= U' <= 0x0F FF FF)
|
||||||
|
// W1 = 110110yyyyyyyyyy
|
||||||
|
// W2 = 110111xxxxxxxxxx
|
||||||
|
//
|
||||||
|
// where U is the character value, W1 is the high surrogate
|
||||||
|
// area, W2 is the low surrogate area.
|
||||||
|
|
||||||
|
// Check for incomplete UTF-16 character.
|
||||||
|
if raw_unread < 2 {
|
||||||
|
if parser.eof {
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"incomplete UTF-16 character",
|
||||||
|
parser.offset, -1)
|
||||||
|
}
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the character.
|
||||||
|
value = rune(parser.raw_buffer[parser.raw_buffer_pos+low]) +
|
||||||
|
(rune(parser.raw_buffer[parser.raw_buffer_pos+high]) << 8)
|
||||||
|
|
||||||
|
// Check for unexpected low surrogate area.
|
||||||
|
if value&0xFC00 == 0xDC00 {
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"unexpected low surrogate area",
|
||||||
|
parser.offset, int(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a high surrogate area.
|
||||||
|
if value&0xFC00 == 0xD800 {
|
||||||
|
width = 4
|
||||||
|
|
||||||
|
// Check for incomplete surrogate pair.
|
||||||
|
if raw_unread < 4 {
|
||||||
|
if parser.eof {
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"incomplete UTF-16 surrogate pair",
|
||||||
|
parser.offset, -1)
|
||||||
|
}
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the next character.
|
||||||
|
value2 := rune(parser.raw_buffer[parser.raw_buffer_pos+low+2]) +
|
||||||
|
(rune(parser.raw_buffer[parser.raw_buffer_pos+high+2]) << 8)
|
||||||
|
|
||||||
|
// Check for a low surrogate area.
|
||||||
|
if value2&0xFC00 != 0xDC00 {
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"expected low surrogate area",
|
||||||
|
parser.offset+2, int(value2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the value of the surrogate pair.
|
||||||
|
value = 0x10000 + ((value & 0x3FF) << 10) + (value2 & 0x3FF)
|
||||||
|
} else {
|
||||||
|
width = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("impossible")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character is in the allowed range:
|
||||||
|
// #x9 | #xA | #xD | [#x20-#x7E] (8 bit)
|
||||||
|
// | #x85 | [#xA0-#xD7FF] | [#xE000-#xFFFD] (16 bit)
|
||||||
|
// | [#x10000-#x10FFFF] (32 bit)
|
||||||
|
switch {
|
||||||
|
case value == 0x09:
|
||||||
|
case value == 0x0A:
|
||||||
|
case value == 0x0D:
|
||||||
|
case value >= 0x20 && value <= 0x7E:
|
||||||
|
case value == 0x85:
|
||||||
|
case value >= 0xA0 && value <= 0xD7FF:
|
||||||
|
case value >= 0xE000 && value <= 0xFFFD:
|
||||||
|
case value >= 0x10000 && value <= 0x10FFFF:
|
||||||
|
default:
|
||||||
|
return yaml_parser_set_reader_error(parser,
|
||||||
|
"control characters are not allowed",
|
||||||
|
parser.offset, int(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the raw pointers.
|
||||||
|
parser.raw_buffer_pos += width
|
||||||
|
parser.offset += width
|
||||||
|
|
||||||
|
// Finally put the character into the buffer.
|
||||||
|
if value <= 0x7F {
|
||||||
|
// 0000 0000-0000 007F . 0xxxxxxx
|
||||||
|
parser.buffer[buffer_len+0] = byte(value)
|
||||||
|
buffer_len += 1
|
||||||
|
} else if value <= 0x7FF {
|
||||||
|
// 0000 0080-0000 07FF . 110xxxxx 10xxxxxx
|
||||||
|
parser.buffer[buffer_len+0] = byte(0xC0 + (value >> 6))
|
||||||
|
parser.buffer[buffer_len+1] = byte(0x80 + (value & 0x3F))
|
||||||
|
buffer_len += 2
|
||||||
|
} else if value <= 0xFFFF {
|
||||||
|
// 0000 0800-0000 FFFF . 1110xxxx 10xxxxxx 10xxxxxx
|
||||||
|
parser.buffer[buffer_len+0] = byte(0xE0 + (value >> 12))
|
||||||
|
parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 6) & 0x3F))
|
||||||
|
parser.buffer[buffer_len+2] = byte(0x80 + (value & 0x3F))
|
||||||
|
buffer_len += 3
|
||||||
|
} else {
|
||||||
|
// 0001 0000-0010 FFFF . 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
||||||
|
parser.buffer[buffer_len+0] = byte(0xF0 + (value >> 18))
|
||||||
|
parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 12) & 0x3F))
|
||||||
|
parser.buffer[buffer_len+2] = byte(0x80 + ((value >> 6) & 0x3F))
|
||||||
|
parser.buffer[buffer_len+3] = byte(0x80 + (value & 0x3F))
|
||||||
|
buffer_len += 4
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.unread++
|
||||||
|
}
|
||||||
|
|
||||||
|
// On EOF, put NUL into the buffer and return.
|
||||||
|
if parser.eof {
|
||||||
|
parser.buffer[buffer_len] = 0
|
||||||
|
buffer_len++
|
||||||
|
parser.unread++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [Go] Read the documentation of this function above. To return true,
|
||||||
|
// we need to have the given length in the buffer. Not doing that means
|
||||||
|
// every single check that calls this function to make sure the buffer
|
||||||
|
// has a given length is Go) panicking; or C) accessing invalid memory.
|
||||||
|
// This happens here due to the EOF above breaking early.
|
||||||
|
for buffer_len < length {
|
||||||
|
parser.buffer[buffer_len] = 0
|
||||||
|
buffer_len++
|
||||||
|
}
|
||||||
|
parser.buffer = parser.buffer[:buffer_len]
|
||||||
|
return true
|
||||||
|
}
|
||||||
326
third_party/yaml.v3/resolve.go
vendored
Normal file
326
third_party/yaml.v3/resolve.go
vendored
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type resolveMapItem struct {
|
||||||
|
value interface{}
|
||||||
|
tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolveTable = make([]byte, 256)
|
||||||
|
var resolveMap = make(map[string]resolveMapItem)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
t := resolveTable
|
||||||
|
t[int('+')] = 'S' // Sign
|
||||||
|
t[int('-')] = 'S'
|
||||||
|
for _, c := range "0123456789" {
|
||||||
|
t[int(c)] = 'D' // Digit
|
||||||
|
}
|
||||||
|
for _, c := range "yYnNtTfFoO~" {
|
||||||
|
t[int(c)] = 'M' // In map
|
||||||
|
}
|
||||||
|
t[int('.')] = '.' // Float (potentially in map)
|
||||||
|
|
||||||
|
var resolveMapList = []struct {
|
||||||
|
v interface{}
|
||||||
|
tag string
|
||||||
|
l []string
|
||||||
|
}{
|
||||||
|
{true, boolTag, []string{"true", "True", "TRUE"}},
|
||||||
|
{false, boolTag, []string{"false", "False", "FALSE"}},
|
||||||
|
{nil, nullTag, []string{"", "~", "null", "Null", "NULL"}},
|
||||||
|
{math.NaN(), floatTag, []string{".nan", ".NaN", ".NAN"}},
|
||||||
|
{math.Inf(+1), floatTag, []string{".inf", ".Inf", ".INF"}},
|
||||||
|
{math.Inf(+1), floatTag, []string{"+.inf", "+.Inf", "+.INF"}},
|
||||||
|
{math.Inf(-1), floatTag, []string{"-.inf", "-.Inf", "-.INF"}},
|
||||||
|
{"<<", mergeTag, []string{"<<"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := resolveMap
|
||||||
|
for _, item := range resolveMapList {
|
||||||
|
for _, s := range item.l {
|
||||||
|
m[s] = resolveMapItem{item.v, item.tag}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
nullTag = "!!null"
|
||||||
|
boolTag = "!!bool"
|
||||||
|
strTag = "!!str"
|
||||||
|
intTag = "!!int"
|
||||||
|
floatTag = "!!float"
|
||||||
|
timestampTag = "!!timestamp"
|
||||||
|
seqTag = "!!seq"
|
||||||
|
mapTag = "!!map"
|
||||||
|
binaryTag = "!!binary"
|
||||||
|
mergeTag = "!!merge"
|
||||||
|
)
|
||||||
|
|
||||||
|
var longTags = make(map[string]string)
|
||||||
|
var shortTags = make(map[string]string)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} {
|
||||||
|
ltag := longTag(stag)
|
||||||
|
longTags[stag] = ltag
|
||||||
|
shortTags[ltag] = stag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const longTagPrefix = "tag:yaml.org,2002:"
|
||||||
|
|
||||||
|
func shortTag(tag string) string {
|
||||||
|
if strings.HasPrefix(tag, longTagPrefix) {
|
||||||
|
if stag, ok := shortTags[tag]; ok {
|
||||||
|
return stag
|
||||||
|
}
|
||||||
|
return "!!" + tag[len(longTagPrefix):]
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func longTag(tag string) string {
|
||||||
|
if strings.HasPrefix(tag, "!!") {
|
||||||
|
if ltag, ok := longTags[tag]; ok {
|
||||||
|
return ltag
|
||||||
|
}
|
||||||
|
return longTagPrefix + tag[2:]
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvableTag(tag string) bool {
|
||||||
|
switch tag {
|
||||||
|
case "", strTag, boolTag, intTag, floatTag, nullTag, timestampTag:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var yamlStyleFloat = regexp.MustCompile(`^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$`)
|
||||||
|
|
||||||
|
func resolve(tag string, in string) (rtag string, out interface{}) {
|
||||||
|
tag = shortTag(tag)
|
||||||
|
if !resolvableTag(tag) {
|
||||||
|
return tag, in
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
switch tag {
|
||||||
|
case "", rtag, strTag, binaryTag:
|
||||||
|
return
|
||||||
|
case floatTag:
|
||||||
|
if rtag == intTag {
|
||||||
|
switch v := out.(type) {
|
||||||
|
case int64:
|
||||||
|
rtag = floatTag
|
||||||
|
out = float64(v)
|
||||||
|
return
|
||||||
|
case int:
|
||||||
|
rtag = floatTag
|
||||||
|
out = float64(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failf("cannot decode %s `%s` as a %s", shortTag(rtag), in, shortTag(tag))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Any data is accepted as a !!str or !!binary.
|
||||||
|
// Otherwise, the prefix is enough of a hint about what it might be.
|
||||||
|
hint := byte('N')
|
||||||
|
if in != "" {
|
||||||
|
hint = resolveTable[in[0]]
|
||||||
|
}
|
||||||
|
if hint != 0 && tag != strTag && tag != binaryTag {
|
||||||
|
// Handle things we can lookup in a map.
|
||||||
|
if item, ok := resolveMap[in]; ok {
|
||||||
|
return item.tag, item.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base 60 floats are a bad idea, were dropped in YAML 1.2, and
|
||||||
|
// are purposefully unsupported here. They're still quoted on
|
||||||
|
// the way out for compatibility with other parser, though.
|
||||||
|
|
||||||
|
switch hint {
|
||||||
|
case 'M':
|
||||||
|
// We've already checked the map above.
|
||||||
|
|
||||||
|
case '.':
|
||||||
|
// Not in the map, so maybe a normal float.
|
||||||
|
floatv, err := strconv.ParseFloat(in, 64)
|
||||||
|
if err == nil {
|
||||||
|
return floatTag, floatv
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'D', 'S':
|
||||||
|
// Int, float, or timestamp.
|
||||||
|
// Only try values as a timestamp if the value is unquoted or there's an explicit
|
||||||
|
// !!timestamp tag.
|
||||||
|
if tag == "" || tag == timestampTag {
|
||||||
|
t, ok := parseTimestamp(in)
|
||||||
|
if ok {
|
||||||
|
return timestampTag, t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plain := strings.Replace(in, "_", "", -1)
|
||||||
|
intv, err := strconv.ParseInt(plain, 0, 64)
|
||||||
|
if err == nil {
|
||||||
|
if intv == int64(int(intv)) {
|
||||||
|
return intTag, int(intv)
|
||||||
|
} else {
|
||||||
|
return intTag, intv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uintv, err := strconv.ParseUint(plain, 0, 64)
|
||||||
|
if err == nil {
|
||||||
|
return intTag, uintv
|
||||||
|
}
|
||||||
|
if yamlStyleFloat.MatchString(plain) {
|
||||||
|
floatv, err := strconv.ParseFloat(plain, 64)
|
||||||
|
if err == nil {
|
||||||
|
return floatTag, floatv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(plain, "0b") {
|
||||||
|
intv, err := strconv.ParseInt(plain[2:], 2, 64)
|
||||||
|
if err == nil {
|
||||||
|
if intv == int64(int(intv)) {
|
||||||
|
return intTag, int(intv)
|
||||||
|
} else {
|
||||||
|
return intTag, intv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uintv, err := strconv.ParseUint(plain[2:], 2, 64)
|
||||||
|
if err == nil {
|
||||||
|
return intTag, uintv
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(plain, "-0b") {
|
||||||
|
intv, err := strconv.ParseInt("-"+plain[3:], 2, 64)
|
||||||
|
if err == nil {
|
||||||
|
if true || intv == int64(int(intv)) {
|
||||||
|
return intTag, int(intv)
|
||||||
|
} else {
|
||||||
|
return intTag, intv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Octals as introduced in version 1.2 of the spec.
|
||||||
|
// Octals from the 1.1 spec, spelled as 0777, are still
|
||||||
|
// decoded by default in v3 as well for compatibility.
|
||||||
|
// May be dropped in v4 depending on how usage evolves.
|
||||||
|
if strings.HasPrefix(plain, "0o") {
|
||||||
|
intv, err := strconv.ParseInt(plain[2:], 8, 64)
|
||||||
|
if err == nil {
|
||||||
|
if intv == int64(int(intv)) {
|
||||||
|
return intTag, int(intv)
|
||||||
|
} else {
|
||||||
|
return intTag, intv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uintv, err := strconv.ParseUint(plain[2:], 8, 64)
|
||||||
|
if err == nil {
|
||||||
|
return intTag, uintv
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(plain, "-0o") {
|
||||||
|
intv, err := strconv.ParseInt("-"+plain[3:], 8, 64)
|
||||||
|
if err == nil {
|
||||||
|
if true || intv == int64(int(intv)) {
|
||||||
|
return intTag, int(intv)
|
||||||
|
} else {
|
||||||
|
return intTag, intv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("internal error: missing handler for resolver table: " + string(rune(hint)) + " (with " + in + ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strTag, in
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeBase64 encodes s as base64 that is broken up into multiple lines
|
||||||
|
// as appropriate for the resulting length.
|
||||||
|
func encodeBase64(s string) string {
|
||||||
|
const lineLen = 70
|
||||||
|
encLen := base64.StdEncoding.EncodedLen(len(s))
|
||||||
|
lines := encLen/lineLen + 1
|
||||||
|
buf := make([]byte, encLen*2+lines)
|
||||||
|
in := buf[0:encLen]
|
||||||
|
out := buf[encLen:]
|
||||||
|
base64.StdEncoding.Encode(in, []byte(s))
|
||||||
|
k := 0
|
||||||
|
for i := 0; i < len(in); i += lineLen {
|
||||||
|
j := i + lineLen
|
||||||
|
if j > len(in) {
|
||||||
|
j = len(in)
|
||||||
|
}
|
||||||
|
k += copy(out[k:], in[i:j])
|
||||||
|
if lines > 1 {
|
||||||
|
out[k] = '\n'
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(out[:k])
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a subset of the formats allowed by the regular expression
|
||||||
|
// defined at http://yaml.org/type/timestamp.html.
|
||||||
|
var allowedTimestampFormats = []string{
|
||||||
|
"2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields.
|
||||||
|
"2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t".
|
||||||
|
"2006-1-2 15:4:5.999999999", // space separated with no time zone
|
||||||
|
"2006-1-2", // date only
|
||||||
|
// Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5"
|
||||||
|
// from the set of examples.
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTimestamp parses s as a timestamp string and
|
||||||
|
// returns the timestamp and reports whether it succeeded.
|
||||||
|
// Timestamp formats are defined at http://yaml.org/type/timestamp.html
|
||||||
|
func parseTimestamp(s string) (time.Time, bool) {
|
||||||
|
// TODO write code to check all the formats supported by
|
||||||
|
// http://yaml.org/type/timestamp.html instead of using time.Parse.
|
||||||
|
|
||||||
|
// Quick check: all date formats start with YYYY-.
|
||||||
|
i := 0
|
||||||
|
for ; i < len(s); i++ {
|
||||||
|
if c := s[i]; c < '0' || c > '9' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i != 4 || i == len(s) || s[i] != '-' {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
for _, format := range allowedTimestampFormats {
|
||||||
|
if t, err := time.Parse(format, s); err == nil {
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
3040
third_party/yaml.v3/scannerc.go
vendored
Normal file
3040
third_party/yaml.v3/scannerc.go
vendored
Normal file
File diff suppressed because it is too large
Load diff
134
third_party/yaml.v3/sorter.go
vendored
Normal file
134
third_party/yaml.v3/sorter.go
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type keyList []reflect.Value
|
||||||
|
|
||||||
|
func (l keyList) Len() int { return len(l) }
|
||||||
|
func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
|
||||||
|
func (l keyList) Less(i, j int) bool {
|
||||||
|
a := l[i]
|
||||||
|
b := l[j]
|
||||||
|
ak := a.Kind()
|
||||||
|
bk := b.Kind()
|
||||||
|
for (ak == reflect.Interface || ak == reflect.Ptr) && !a.IsNil() {
|
||||||
|
a = a.Elem()
|
||||||
|
ak = a.Kind()
|
||||||
|
}
|
||||||
|
for (bk == reflect.Interface || bk == reflect.Ptr) && !b.IsNil() {
|
||||||
|
b = b.Elem()
|
||||||
|
bk = b.Kind()
|
||||||
|
}
|
||||||
|
af, aok := keyFloat(a)
|
||||||
|
bf, bok := keyFloat(b)
|
||||||
|
if aok && bok {
|
||||||
|
if af != bf {
|
||||||
|
return af < bf
|
||||||
|
}
|
||||||
|
if ak != bk {
|
||||||
|
return ak < bk
|
||||||
|
}
|
||||||
|
return numLess(a, b)
|
||||||
|
}
|
||||||
|
if ak != reflect.String || bk != reflect.String {
|
||||||
|
return ak < bk
|
||||||
|
}
|
||||||
|
ar, br := []rune(a.String()), []rune(b.String())
|
||||||
|
digits := false
|
||||||
|
for i := 0; i < len(ar) && i < len(br); i++ {
|
||||||
|
if ar[i] == br[i] {
|
||||||
|
digits = unicode.IsDigit(ar[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
al := unicode.IsLetter(ar[i])
|
||||||
|
bl := unicode.IsLetter(br[i])
|
||||||
|
if al && bl {
|
||||||
|
return ar[i] < br[i]
|
||||||
|
}
|
||||||
|
if al || bl {
|
||||||
|
if digits {
|
||||||
|
return al
|
||||||
|
} else {
|
||||||
|
return bl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ai, bi int
|
||||||
|
var an, bn int64
|
||||||
|
if ar[i] == '0' || br[i] == '0' {
|
||||||
|
for j := i - 1; j >= 0 && unicode.IsDigit(ar[j]); j-- {
|
||||||
|
if ar[j] != '0' {
|
||||||
|
an = 1
|
||||||
|
bn = 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ai = i; ai < len(ar) && unicode.IsDigit(ar[ai]); ai++ {
|
||||||
|
an = an*10 + int64(ar[ai]-'0')
|
||||||
|
}
|
||||||
|
for bi = i; bi < len(br) && unicode.IsDigit(br[bi]); bi++ {
|
||||||
|
bn = bn*10 + int64(br[bi]-'0')
|
||||||
|
}
|
||||||
|
if an != bn {
|
||||||
|
return an < bn
|
||||||
|
}
|
||||||
|
if ai != bi {
|
||||||
|
return ai < bi
|
||||||
|
}
|
||||||
|
return ar[i] < br[i]
|
||||||
|
}
|
||||||
|
return len(ar) < len(br)
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyFloat returns a float value for v if it is a number/bool
|
||||||
|
// and whether it is a number/bool or not.
|
||||||
|
func keyFloat(v reflect.Value) (f float64, ok bool) {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return float64(v.Int()), true
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return v.Float(), true
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return float64(v.Uint()), true
|
||||||
|
case reflect.Bool:
|
||||||
|
if v.Bool() {
|
||||||
|
return 1, true
|
||||||
|
}
|
||||||
|
return 0, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// numLess returns whether a < b.
|
||||||
|
// a and b must necessarily have the same kind.
|
||||||
|
func numLess(a, b reflect.Value) bool {
|
||||||
|
switch a.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return a.Int() < b.Int()
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return a.Float() < b.Float()
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return a.Uint() < b.Uint()
|
||||||
|
case reflect.Bool:
|
||||||
|
return !a.Bool() && b.Bool()
|
||||||
|
}
|
||||||
|
panic("not a number")
|
||||||
|
}
|
||||||
48
third_party/yaml.v3/writerc.go
vendored
Normal file
48
third_party/yaml.v3/writerc.go
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
// Copyright (c) 2006-2010 Kirill Simonov
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
// so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
// Set the writer error and return false.
|
||||||
|
func yaml_emitter_set_writer_error(emitter *yaml_emitter_t, problem string) bool {
|
||||||
|
emitter.error = yaml_WRITER_ERROR
|
||||||
|
emitter.problem = problem
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the output buffer.
|
||||||
|
func yaml_emitter_flush(emitter *yaml_emitter_t) bool {
|
||||||
|
if emitter.write_handler == nil {
|
||||||
|
panic("write handler not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the buffer is empty.
|
||||||
|
if emitter.buffer_pos == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil {
|
||||||
|
return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error())
|
||||||
|
}
|
||||||
|
emitter.buffer_pos = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
693
third_party/yaml.v3/yaml.go
vendored
Normal file
693
third_party/yaml.v3/yaml.go
vendored
Normal file
|
|
@ -0,0 +1,693 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package yaml implements YAML support for the Go language.
|
||||||
|
//
|
||||||
|
// Source code and other details for the project are available at GitHub:
|
||||||
|
//
|
||||||
|
// https://github.com/go-yaml/yaml
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Unmarshaler interface may be implemented by types to customize their
|
||||||
|
// behavior when being unmarshaled from a YAML document.
|
||||||
|
type Unmarshaler interface {
|
||||||
|
UnmarshalYAML(value *Node) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type obsoleteUnmarshaler interface {
|
||||||
|
UnmarshalYAML(unmarshal func(interface{}) error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Marshaler interface may be implemented by types to customize their
|
||||||
|
// behavior when being marshaled into a YAML document. The returned value
|
||||||
|
// is marshaled in place of the original value implementing Marshaler.
|
||||||
|
//
|
||||||
|
// If an error is returned by MarshalYAML, the marshaling procedure stops
|
||||||
|
// and returns with the provided error.
|
||||||
|
type Marshaler interface {
|
||||||
|
MarshalYAML() (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal decodes the first document found within the in byte slice
|
||||||
|
// and assigns decoded values into the out value.
|
||||||
|
//
|
||||||
|
// Maps and pointers (to a struct, string, int, etc) are accepted as out
|
||||||
|
// values. If an internal pointer within a struct is not initialized,
|
||||||
|
// the yaml package will initialize it if necessary for unmarshalling
|
||||||
|
// the provided data. The out parameter must not be nil.
|
||||||
|
//
|
||||||
|
// The type of the decoded values should be compatible with the respective
|
||||||
|
// values in out. If one or more values cannot be decoded due to a type
|
||||||
|
// mismatches, decoding continues partially until the end of the YAML
|
||||||
|
// content, and a *yaml.TypeError is returned with details for all
|
||||||
|
// missed values.
|
||||||
|
//
|
||||||
|
// Struct fields are only unmarshalled if they are exported (have an
|
||||||
|
// upper case first letter), and are unmarshalled using the field name
|
||||||
|
// lowercased as the default key. Custom keys may be defined via the
|
||||||
|
// "yaml" name in the field tag: the content preceding the first comma
|
||||||
|
// is used as the key, and the following comma-separated options are
|
||||||
|
// used to tweak the marshalling process (see Marshal).
|
||||||
|
// Conflicting names result in a runtime error.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// type T struct {
|
||||||
|
// F int `yaml:"a,omitempty"`
|
||||||
|
// B int
|
||||||
|
// }
|
||||||
|
// var t T
|
||||||
|
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
|
||||||
|
//
|
||||||
|
// See the documentation of Marshal for the format of tags and a list of
|
||||||
|
// supported tag options.
|
||||||
|
func Unmarshal(in []byte, out interface{}) (err error) {
|
||||||
|
return unmarshal(in, out, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Decoder reads and decodes YAML values from an input stream.
|
||||||
|
type Decoder struct {
|
||||||
|
parser *parser
|
||||||
|
knownFields bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDecoder returns a new decoder that reads from r.
|
||||||
|
//
|
||||||
|
// The decoder introduces its own buffering and may read
|
||||||
|
// data from r beyond the YAML values requested.
|
||||||
|
func NewDecoder(r io.Reader) *Decoder {
|
||||||
|
return &Decoder{
|
||||||
|
parser: newParserFromReader(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnownFields ensures that the keys in decoded mappings to
|
||||||
|
// exist as fields in the struct being decoded into.
|
||||||
|
func (dec *Decoder) KnownFields(enable bool) {
|
||||||
|
dec.knownFields = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode reads the next YAML-encoded value from its input
|
||||||
|
// and stores it in the value pointed to by v.
|
||||||
|
//
|
||||||
|
// See the documentation for Unmarshal for details about the
|
||||||
|
// conversion of YAML into a Go value.
|
||||||
|
func (dec *Decoder) Decode(v interface{}) (err error) {
|
||||||
|
d := newDecoder()
|
||||||
|
d.knownFields = dec.knownFields
|
||||||
|
defer handleErr(&err)
|
||||||
|
node := dec.parser.parse()
|
||||||
|
if node == nil {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
out := reflect.ValueOf(v)
|
||||||
|
if out.Kind() == reflect.Ptr && !out.IsNil() {
|
||||||
|
out = out.Elem()
|
||||||
|
}
|
||||||
|
d.unmarshal(node, out)
|
||||||
|
if len(d.terrors) > 0 {
|
||||||
|
return &TypeError{d.terrors}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes the node and stores its data into the value pointed to by v.
|
||||||
|
//
|
||||||
|
// See the documentation for Unmarshal for details about the
|
||||||
|
// conversion of YAML into a Go value.
|
||||||
|
func (n *Node) Decode(v interface{}) (err error) {
|
||||||
|
d := newDecoder()
|
||||||
|
defer handleErr(&err)
|
||||||
|
out := reflect.ValueOf(v)
|
||||||
|
if out.Kind() == reflect.Ptr && !out.IsNil() {
|
||||||
|
out = out.Elem()
|
||||||
|
}
|
||||||
|
d.unmarshal(n, out)
|
||||||
|
if len(d.terrors) > 0 {
|
||||||
|
return &TypeError{d.terrors}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshal(in []byte, out interface{}, strict bool) (err error) {
|
||||||
|
defer handleErr(&err)
|
||||||
|
d := newDecoder()
|
||||||
|
p := newParser(in)
|
||||||
|
defer p.destroy()
|
||||||
|
node := p.parse()
|
||||||
|
if node != nil {
|
||||||
|
v := reflect.ValueOf(out)
|
||||||
|
if v.Kind() == reflect.Ptr && !v.IsNil() {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
d.unmarshal(node, v)
|
||||||
|
}
|
||||||
|
if len(d.terrors) > 0 {
|
||||||
|
return &TypeError{d.terrors}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal serializes the value provided into a YAML document. The structure
|
||||||
|
// of the generated document will reflect the structure of the value itself.
|
||||||
|
// Maps and pointers (to struct, string, int, etc) are accepted as the in value.
|
||||||
|
//
|
||||||
|
// Struct fields are only marshalled if they are exported (have an upper case
|
||||||
|
// first letter), and are marshalled using the field name lowercased as the
|
||||||
|
// default key. Custom keys may be defined via the "yaml" name in the field
|
||||||
|
// tag: the content preceding the first comma is used as the key, and the
|
||||||
|
// following comma-separated options are used to tweak the marshalling process.
|
||||||
|
// Conflicting names result in a runtime error.
|
||||||
|
//
|
||||||
|
// The field tag format accepted is:
|
||||||
|
//
|
||||||
|
// `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
|
||||||
|
//
|
||||||
|
// The following flags are currently supported:
|
||||||
|
//
|
||||||
|
// omitempty Only include the field if it's not set to the zero
|
||||||
|
// value for the type or to empty slices or maps.
|
||||||
|
// Zero valued structs will be omitted if all their public
|
||||||
|
// fields are zero, unless they implement an IsZero
|
||||||
|
// method (see the IsZeroer interface type), in which
|
||||||
|
// case the field will be excluded if IsZero returns true.
|
||||||
|
//
|
||||||
|
// flow Marshal using a flow style (useful for structs,
|
||||||
|
// sequences and maps).
|
||||||
|
//
|
||||||
|
// inline Inline the field, which must be a struct or a map,
|
||||||
|
// causing all of its fields or keys to be processed as if
|
||||||
|
// they were part of the outer struct. For maps, keys must
|
||||||
|
// not conflict with the yaml keys of other struct fields.
|
||||||
|
//
|
||||||
|
// In addition, if the key is "-", the field is ignored.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// type T struct {
|
||||||
|
// F int `yaml:"a,omitempty"`
|
||||||
|
// B int
|
||||||
|
// }
|
||||||
|
// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n"
|
||||||
|
// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n"
|
||||||
|
func Marshal(in interface{}) (out []byte, err error) {
|
||||||
|
defer handleErr(&err)
|
||||||
|
e := newEncoder()
|
||||||
|
defer e.destroy()
|
||||||
|
e.marshalDoc("", reflect.ValueOf(in))
|
||||||
|
e.finish()
|
||||||
|
out = e.out
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Encoder writes YAML values to an output stream.
|
||||||
|
type Encoder struct {
|
||||||
|
encoder *encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncoder returns a new encoder that writes to w.
|
||||||
|
// The Encoder should be closed after use to flush all data
|
||||||
|
// to w.
|
||||||
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
|
return &Encoder{
|
||||||
|
encoder: newEncoderWithWriter(w),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode writes the YAML encoding of v to the stream.
|
||||||
|
// If multiple items are encoded to the stream, the
|
||||||
|
// second and subsequent document will be preceded
|
||||||
|
// with a "---" document separator, but the first will not.
|
||||||
|
//
|
||||||
|
// See the documentation for Marshal for details about the conversion of Go
|
||||||
|
// values to YAML.
|
||||||
|
func (e *Encoder) Encode(v interface{}) (err error) {
|
||||||
|
defer handleErr(&err)
|
||||||
|
e.encoder.marshalDoc("", reflect.ValueOf(v))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes value v and stores its representation in n.
|
||||||
|
//
|
||||||
|
// See the documentation for Marshal for details about the
|
||||||
|
// conversion of Go values into YAML.
|
||||||
|
func (n *Node) Encode(v interface{}) (err error) {
|
||||||
|
defer handleErr(&err)
|
||||||
|
e := newEncoder()
|
||||||
|
defer e.destroy()
|
||||||
|
e.marshalDoc("", reflect.ValueOf(v))
|
||||||
|
e.finish()
|
||||||
|
p := newParser(e.out)
|
||||||
|
p.textless = true
|
||||||
|
defer p.destroy()
|
||||||
|
doc := p.parse()
|
||||||
|
*n = *doc.Content[0]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIndent changes the used indentation used when encoding.
|
||||||
|
func (e *Encoder) SetIndent(spaces int) {
|
||||||
|
if spaces < 0 {
|
||||||
|
panic("yaml: cannot indent to a negative number of spaces")
|
||||||
|
}
|
||||||
|
e.encoder.indent = spaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the encoder by writing any remaining data.
|
||||||
|
// It does not write a stream terminating string "...".
|
||||||
|
func (e *Encoder) Close() (err error) {
|
||||||
|
defer handleErr(&err)
|
||||||
|
e.encoder.finish()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleErr(err *error) {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
if e, ok := v.(yamlError); ok {
|
||||||
|
*err = e.err
|
||||||
|
} else {
|
||||||
|
panic(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type yamlError struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(err error) {
|
||||||
|
panic(yamlError{err})
|
||||||
|
}
|
||||||
|
|
||||||
|
func failf(format string, args ...interface{}) {
|
||||||
|
panic(yamlError{fmt.Errorf("yaml: "+format, args...)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TypeError is returned by Unmarshal when one or more fields in
|
||||||
|
// the YAML document cannot be properly decoded into the requested
|
||||||
|
// types. When this error is returned, the value is still
|
||||||
|
// unmarshaled partially.
|
||||||
|
type TypeError struct {
|
||||||
|
Errors []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TypeError) Error() string {
|
||||||
|
return fmt.Sprintf("yaml: unmarshal errors:\n %s", strings.Join(e.Errors, "\n "))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Kind uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
DocumentNode Kind = 1 << iota
|
||||||
|
SequenceNode
|
||||||
|
MappingNode
|
||||||
|
ScalarNode
|
||||||
|
AliasNode
|
||||||
|
)
|
||||||
|
|
||||||
|
type Style uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaggedStyle Style = 1 << iota
|
||||||
|
DoubleQuotedStyle
|
||||||
|
SingleQuotedStyle
|
||||||
|
LiteralStyle
|
||||||
|
FoldedStyle
|
||||||
|
FlowStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
// Node represents an element in the YAML document hierarchy. While documents
|
||||||
|
// are typically encoded and decoded into higher level types, such as structs
|
||||||
|
// and maps, Node is an intermediate representation that allows detailed
|
||||||
|
// control over the content being decoded or encoded.
|
||||||
|
//
|
||||||
|
// It's worth noting that although Node offers access into details such as
|
||||||
|
// line numbers, colums, and comments, the content when re-encoded will not
|
||||||
|
// have its original textual representation preserved. An effort is made to
|
||||||
|
// render the data plesantly, and to preserve comments near the data they
|
||||||
|
// describe, though.
|
||||||
|
//
|
||||||
|
// Values that make use of the Node type interact with the yaml package in the
|
||||||
|
// same way any other type would do, by encoding and decoding yaml data
|
||||||
|
// directly or indirectly into them.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// var person struct {
|
||||||
|
// Name string
|
||||||
|
// Address yaml.Node
|
||||||
|
// }
|
||||||
|
// err := yaml.Unmarshal(data, &person)
|
||||||
|
//
|
||||||
|
// Or by itself:
|
||||||
|
//
|
||||||
|
// var person Node
|
||||||
|
// err := yaml.Unmarshal(data, &person)
|
||||||
|
type Node struct {
|
||||||
|
// Kind defines whether the node is a document, a mapping, a sequence,
|
||||||
|
// a scalar value, or an alias to another node. The specific data type of
|
||||||
|
// scalar nodes may be obtained via the ShortTag and LongTag methods.
|
||||||
|
Kind Kind
|
||||||
|
|
||||||
|
// Style allows customizing the apperance of the node in the tree.
|
||||||
|
Style Style
|
||||||
|
|
||||||
|
// Tag holds the YAML tag defining the data type for the value.
|
||||||
|
// When decoding, this field will always be set to the resolved tag,
|
||||||
|
// even when it wasn't explicitly provided in the YAML content.
|
||||||
|
// When encoding, if this field is unset the value type will be
|
||||||
|
// implied from the node properties, and if it is set, it will only
|
||||||
|
// be serialized into the representation if TaggedStyle is used or
|
||||||
|
// the implicit tag diverges from the provided one.
|
||||||
|
Tag string
|
||||||
|
|
||||||
|
// Value holds the unescaped and unquoted represenation of the value.
|
||||||
|
Value string
|
||||||
|
|
||||||
|
// Anchor holds the anchor name for this node, which allows aliases to point to it.
|
||||||
|
Anchor string
|
||||||
|
|
||||||
|
// Alias holds the node that this alias points to. Only valid when Kind is AliasNode.
|
||||||
|
Alias *Node
|
||||||
|
|
||||||
|
// Content holds contained nodes for documents, mappings, and sequences.
|
||||||
|
Content []*Node
|
||||||
|
|
||||||
|
// HeadComment holds any comments in the lines preceding the node and
|
||||||
|
// not separated by an empty line.
|
||||||
|
HeadComment string
|
||||||
|
|
||||||
|
// LineComment holds any comments at the end of the line where the node is in.
|
||||||
|
LineComment string
|
||||||
|
|
||||||
|
// FootComment holds any comments following the node and before empty lines.
|
||||||
|
FootComment string
|
||||||
|
|
||||||
|
// Line and Column hold the node position in the decoded YAML text.
|
||||||
|
// These fields are not respected when encoding the node.
|
||||||
|
Line int
|
||||||
|
Column int
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns whether the node has all of its fields unset.
|
||||||
|
func (n *Node) IsZero() bool {
|
||||||
|
return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil &&
|
||||||
|
n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// LongTag returns the long form of the tag that indicates the data type for
|
||||||
|
// the node. If the Tag field isn't explicitly defined, one will be computed
|
||||||
|
// based on the node properties.
|
||||||
|
func (n *Node) LongTag() string {
|
||||||
|
return longTag(n.ShortTag())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortTag returns the short form of the YAML tag that indicates data type for
|
||||||
|
// the node. If the Tag field isn't explicitly defined, one will be computed
|
||||||
|
// based on the node properties.
|
||||||
|
func (n *Node) ShortTag() string {
|
||||||
|
if n.indicatedString() {
|
||||||
|
return strTag
|
||||||
|
}
|
||||||
|
if n.Tag == "" || n.Tag == "!" {
|
||||||
|
switch n.Kind {
|
||||||
|
case MappingNode:
|
||||||
|
return mapTag
|
||||||
|
case SequenceNode:
|
||||||
|
return seqTag
|
||||||
|
case AliasNode:
|
||||||
|
if n.Alias != nil {
|
||||||
|
return n.Alias.ShortTag()
|
||||||
|
}
|
||||||
|
case ScalarNode:
|
||||||
|
tag, _ := resolve("", n.Value)
|
||||||
|
return tag
|
||||||
|
case 0:
|
||||||
|
// Special case to make the zero value convenient.
|
||||||
|
if n.IsZero() {
|
||||||
|
return nullTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return shortTag(n.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) indicatedString() bool {
|
||||||
|
return n.Kind == ScalarNode &&
|
||||||
|
(shortTag(n.Tag) == strTag ||
|
||||||
|
(n.Tag == "" || n.Tag == "!") && n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetString is a convenience function that sets the node to a string value
|
||||||
|
// and defines its style in a pleasant way depending on its content.
|
||||||
|
func (n *Node) SetString(s string) {
|
||||||
|
n.Kind = ScalarNode
|
||||||
|
if utf8.ValidString(s) {
|
||||||
|
n.Value = s
|
||||||
|
n.Tag = strTag
|
||||||
|
} else {
|
||||||
|
n.Value = encodeBase64(s)
|
||||||
|
n.Tag = binaryTag
|
||||||
|
}
|
||||||
|
if strings.Contains(n.Value, "\n") {
|
||||||
|
n.Style = LiteralStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Maintain a mapping of keys to structure field indexes
|
||||||
|
|
||||||
|
// The code in this section was copied from mgo/bson.
|
||||||
|
|
||||||
|
// structInfo holds details for the serialization of fields of
|
||||||
|
// a given struct.
|
||||||
|
type structInfo struct {
|
||||||
|
FieldsMap map[string]fieldInfo
|
||||||
|
FieldsList []fieldInfo
|
||||||
|
|
||||||
|
// InlineMap is the number of the field in the struct that
|
||||||
|
// contains an ,inline map, or -1 if there's none.
|
||||||
|
InlineMap int
|
||||||
|
|
||||||
|
// InlineUnmarshalers holds indexes to inlined fields that
|
||||||
|
// contain unmarshaler values.
|
||||||
|
InlineUnmarshalers [][]int
|
||||||
|
}
|
||||||
|
|
||||||
|
type fieldInfo struct {
|
||||||
|
Key string
|
||||||
|
Num int
|
||||||
|
OmitEmpty bool
|
||||||
|
Flow bool
|
||||||
|
// Id holds the unique field identifier, so we can cheaply
|
||||||
|
// check for field duplicates without maintaining an extra map.
|
||||||
|
Id int
|
||||||
|
|
||||||
|
// Inline holds the field index if the field is part of an inlined struct.
|
||||||
|
Inline []int
|
||||||
|
}
|
||||||
|
|
||||||
|
var structMap = make(map[reflect.Type]*structInfo)
|
||||||
|
var fieldMapMutex sync.RWMutex
|
||||||
|
var unmarshalerType reflect.Type
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var v Unmarshaler
|
||||||
|
unmarshalerType = reflect.ValueOf(&v).Elem().Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStructInfo(st reflect.Type) (*structInfo, error) {
|
||||||
|
fieldMapMutex.RLock()
|
||||||
|
sinfo, found := structMap[st]
|
||||||
|
fieldMapMutex.RUnlock()
|
||||||
|
if found {
|
||||||
|
return sinfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n := st.NumField()
|
||||||
|
fieldsMap := make(map[string]fieldInfo)
|
||||||
|
fieldsList := make([]fieldInfo, 0, n)
|
||||||
|
inlineMap := -1
|
||||||
|
inlineUnmarshalers := [][]int(nil)
|
||||||
|
for i := 0; i != n; i++ {
|
||||||
|
field := st.Field(i)
|
||||||
|
if field.PkgPath != "" && !field.Anonymous {
|
||||||
|
continue // Private field
|
||||||
|
}
|
||||||
|
|
||||||
|
info := fieldInfo{Num: i}
|
||||||
|
|
||||||
|
tag := field.Tag.Get("yaml")
|
||||||
|
if tag == "" && strings.Index(string(field.Tag), ":") < 0 {
|
||||||
|
tag = string(field.Tag)
|
||||||
|
}
|
||||||
|
if tag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inline := false
|
||||||
|
fields := strings.Split(tag, ",")
|
||||||
|
if len(fields) > 1 {
|
||||||
|
for _, flag := range fields[1:] {
|
||||||
|
switch flag {
|
||||||
|
case "omitempty":
|
||||||
|
info.OmitEmpty = true
|
||||||
|
case "flow":
|
||||||
|
info.Flow = true
|
||||||
|
case "inline":
|
||||||
|
inline = true
|
||||||
|
default:
|
||||||
|
return nil, errors.New(fmt.Sprintf("unsupported flag %q in tag %q of type %s", flag, tag, st))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag = fields[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if inline {
|
||||||
|
switch field.Type.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
if inlineMap >= 0 {
|
||||||
|
return nil, errors.New("multiple ,inline maps in struct " + st.String())
|
||||||
|
}
|
||||||
|
if field.Type.Key() != reflect.TypeOf("") {
|
||||||
|
return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String())
|
||||||
|
}
|
||||||
|
inlineMap = info.Num
|
||||||
|
case reflect.Struct, reflect.Ptr:
|
||||||
|
ftype := field.Type
|
||||||
|
for ftype.Kind() == reflect.Ptr {
|
||||||
|
ftype = ftype.Elem()
|
||||||
|
}
|
||||||
|
if ftype.Kind() != reflect.Struct {
|
||||||
|
return nil, errors.New("option ,inline may only be used on a struct or map field")
|
||||||
|
}
|
||||||
|
if reflect.PtrTo(ftype).Implements(unmarshalerType) {
|
||||||
|
inlineUnmarshalers = append(inlineUnmarshalers, []int{i})
|
||||||
|
} else {
|
||||||
|
sinfo, err := getStructInfo(ftype)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, index := range sinfo.InlineUnmarshalers {
|
||||||
|
inlineUnmarshalers = append(inlineUnmarshalers, append([]int{i}, index...))
|
||||||
|
}
|
||||||
|
for _, finfo := range sinfo.FieldsList {
|
||||||
|
if _, found := fieldsMap[finfo.Key]; found {
|
||||||
|
msg := "duplicated key '" + finfo.Key + "' in struct " + st.String()
|
||||||
|
return nil, errors.New(msg)
|
||||||
|
}
|
||||||
|
if finfo.Inline == nil {
|
||||||
|
finfo.Inline = []int{i, finfo.Num}
|
||||||
|
} else {
|
||||||
|
finfo.Inline = append([]int{i}, finfo.Inline...)
|
||||||
|
}
|
||||||
|
finfo.Id = len(fieldsList)
|
||||||
|
fieldsMap[finfo.Key] = finfo
|
||||||
|
fieldsList = append(fieldsList, finfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("option ,inline may only be used on a struct or map field")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag != "" {
|
||||||
|
info.Key = tag
|
||||||
|
} else {
|
||||||
|
info.Key = strings.ToLower(field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found = fieldsMap[info.Key]; found {
|
||||||
|
msg := "duplicated key '" + info.Key + "' in struct " + st.String()
|
||||||
|
return nil, errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Id = len(fieldsList)
|
||||||
|
fieldsList = append(fieldsList, info)
|
||||||
|
fieldsMap[info.Key] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
sinfo = &structInfo{
|
||||||
|
FieldsMap: fieldsMap,
|
||||||
|
FieldsList: fieldsList,
|
||||||
|
InlineMap: inlineMap,
|
||||||
|
InlineUnmarshalers: inlineUnmarshalers,
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldMapMutex.Lock()
|
||||||
|
structMap[st] = sinfo
|
||||||
|
fieldMapMutex.Unlock()
|
||||||
|
return sinfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZeroer is used to check whether an object is zero to
|
||||||
|
// determine whether it should be omitted when marshaling
|
||||||
|
// with the omitempty flag. One notable implementation
|
||||||
|
// is time.Time.
|
||||||
|
type IsZeroer interface {
|
||||||
|
IsZero() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZero(v reflect.Value) bool {
|
||||||
|
kind := v.Kind()
|
||||||
|
if z, ok := v.Interface().(IsZeroer); ok {
|
||||||
|
if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return z.IsZero()
|
||||||
|
}
|
||||||
|
switch kind {
|
||||||
|
case reflect.String:
|
||||||
|
return len(v.String()) == 0
|
||||||
|
case reflect.Interface, reflect.Ptr:
|
||||||
|
return v.IsNil()
|
||||||
|
case reflect.Slice:
|
||||||
|
return v.Len() == 0
|
||||||
|
case reflect.Map:
|
||||||
|
return v.Len() == 0
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return v.Int() == 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return v.Float() == 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return v.Uint() == 0
|
||||||
|
case reflect.Bool:
|
||||||
|
return !v.Bool()
|
||||||
|
case reflect.Struct:
|
||||||
|
vt := v.Type()
|
||||||
|
for i := v.NumField() - 1; i >= 0; i-- {
|
||||||
|
if vt.Field(i).PkgPath != "" {
|
||||||
|
continue // Private field
|
||||||
|
}
|
||||||
|
if !isZero(v.Field(i)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
809
third_party/yaml.v3/yamlh.go
vendored
Normal file
809
third_party/yaml.v3/yamlh.go
vendored
Normal file
|
|
@ -0,0 +1,809 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
// Copyright (c) 2006-2010 Kirill Simonov
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
// so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The version directive data.
|
||||||
|
type yaml_version_directive_t struct {
|
||||||
|
major int8 // The major version number.
|
||||||
|
minor int8 // The minor version number.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tag directive data.
|
||||||
|
type yaml_tag_directive_t struct {
|
||||||
|
handle []byte // The tag handle.
|
||||||
|
prefix []byte // The tag prefix.
|
||||||
|
}
|
||||||
|
|
||||||
|
type yaml_encoding_t int
|
||||||
|
|
||||||
|
// The stream encoding.
|
||||||
|
const (
|
||||||
|
// Let the parser choose the encoding.
|
||||||
|
yaml_ANY_ENCODING yaml_encoding_t = iota
|
||||||
|
|
||||||
|
yaml_UTF8_ENCODING // The default UTF-8 encoding.
|
||||||
|
yaml_UTF16LE_ENCODING // The UTF-16-LE encoding with BOM.
|
||||||
|
yaml_UTF16BE_ENCODING // The UTF-16-BE encoding with BOM.
|
||||||
|
)
|
||||||
|
|
||||||
|
type yaml_break_t int
|
||||||
|
|
||||||
|
// Line break types.
|
||||||
|
const (
|
||||||
|
// Let the parser choose the break type.
|
||||||
|
yaml_ANY_BREAK yaml_break_t = iota
|
||||||
|
|
||||||
|
yaml_CR_BREAK // Use CR for line breaks (Mac style).
|
||||||
|
yaml_LN_BREAK // Use LN for line breaks (Unix style).
|
||||||
|
yaml_CRLN_BREAK // Use CR LN for line breaks (DOS style).
|
||||||
|
)
|
||||||
|
|
||||||
|
type yaml_error_type_t int
|
||||||
|
|
||||||
|
// Many bad things could happen with the parser and emitter.
|
||||||
|
const (
|
||||||
|
// No error is produced.
|
||||||
|
yaml_NO_ERROR yaml_error_type_t = iota
|
||||||
|
|
||||||
|
yaml_MEMORY_ERROR // Cannot allocate or reallocate a block of memory.
|
||||||
|
yaml_READER_ERROR // Cannot read or decode the input stream.
|
||||||
|
yaml_SCANNER_ERROR // Cannot scan the input stream.
|
||||||
|
yaml_PARSER_ERROR // Cannot parse the input stream.
|
||||||
|
yaml_COMPOSER_ERROR // Cannot compose a YAML document.
|
||||||
|
yaml_WRITER_ERROR // Cannot write to the output stream.
|
||||||
|
yaml_EMITTER_ERROR // Cannot emit a YAML stream.
|
||||||
|
)
|
||||||
|
|
||||||
|
// The pointer position.
|
||||||
|
type yaml_mark_t struct {
|
||||||
|
index int // The position index.
|
||||||
|
line int // The position line.
|
||||||
|
column int // The position column.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node Styles
|
||||||
|
|
||||||
|
type yaml_style_t int8
|
||||||
|
|
||||||
|
type yaml_scalar_style_t yaml_style_t
|
||||||
|
|
||||||
|
// Scalar styles.
|
||||||
|
const (
|
||||||
|
// Let the emitter choose the style.
|
||||||
|
yaml_ANY_SCALAR_STYLE yaml_scalar_style_t = 0
|
||||||
|
|
||||||
|
yaml_PLAIN_SCALAR_STYLE yaml_scalar_style_t = 1 << iota // The plain scalar style.
|
||||||
|
yaml_SINGLE_QUOTED_SCALAR_STYLE // The single-quoted scalar style.
|
||||||
|
yaml_DOUBLE_QUOTED_SCALAR_STYLE // The double-quoted scalar style.
|
||||||
|
yaml_LITERAL_SCALAR_STYLE // The literal scalar style.
|
||||||
|
yaml_FOLDED_SCALAR_STYLE // The folded scalar style.
|
||||||
|
)
|
||||||
|
|
||||||
|
type yaml_sequence_style_t yaml_style_t
|
||||||
|
|
||||||
|
// Sequence styles.
|
||||||
|
const (
|
||||||
|
// Let the emitter choose the style.
|
||||||
|
yaml_ANY_SEQUENCE_STYLE yaml_sequence_style_t = iota
|
||||||
|
|
||||||
|
yaml_BLOCK_SEQUENCE_STYLE // The block sequence style.
|
||||||
|
yaml_FLOW_SEQUENCE_STYLE // The flow sequence style.
|
||||||
|
)
|
||||||
|
|
||||||
|
type yaml_mapping_style_t yaml_style_t
|
||||||
|
|
||||||
|
// Mapping styles.
|
||||||
|
const (
|
||||||
|
// Let the emitter choose the style.
|
||||||
|
yaml_ANY_MAPPING_STYLE yaml_mapping_style_t = iota
|
||||||
|
|
||||||
|
yaml_BLOCK_MAPPING_STYLE // The block mapping style.
|
||||||
|
yaml_FLOW_MAPPING_STYLE // The flow mapping style.
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tokens
|
||||||
|
|
||||||
|
type yaml_token_type_t int
|
||||||
|
|
||||||
|
// Token types.
|
||||||
|
const (
|
||||||
|
// An empty token.
|
||||||
|
yaml_NO_TOKEN yaml_token_type_t = iota
|
||||||
|
|
||||||
|
yaml_STREAM_START_TOKEN // A STREAM-START token.
|
||||||
|
yaml_STREAM_END_TOKEN // A STREAM-END token.
|
||||||
|
|
||||||
|
yaml_VERSION_DIRECTIVE_TOKEN // A VERSION-DIRECTIVE token.
|
||||||
|
yaml_TAG_DIRECTIVE_TOKEN // A TAG-DIRECTIVE token.
|
||||||
|
yaml_DOCUMENT_START_TOKEN // A DOCUMENT-START token.
|
||||||
|
yaml_DOCUMENT_END_TOKEN // A DOCUMENT-END token.
|
||||||
|
|
||||||
|
yaml_BLOCK_SEQUENCE_START_TOKEN // A BLOCK-SEQUENCE-START token.
|
||||||
|
yaml_BLOCK_MAPPING_START_TOKEN // A BLOCK-SEQUENCE-END token.
|
||||||
|
yaml_BLOCK_END_TOKEN // A BLOCK-END token.
|
||||||
|
|
||||||
|
yaml_FLOW_SEQUENCE_START_TOKEN // A FLOW-SEQUENCE-START token.
|
||||||
|
yaml_FLOW_SEQUENCE_END_TOKEN // A FLOW-SEQUENCE-END token.
|
||||||
|
yaml_FLOW_MAPPING_START_TOKEN // A FLOW-MAPPING-START token.
|
||||||
|
yaml_FLOW_MAPPING_END_TOKEN // A FLOW-MAPPING-END token.
|
||||||
|
|
||||||
|
yaml_BLOCK_ENTRY_TOKEN // A BLOCK-ENTRY token.
|
||||||
|
yaml_FLOW_ENTRY_TOKEN // A FLOW-ENTRY token.
|
||||||
|
yaml_KEY_TOKEN // A KEY token.
|
||||||
|
yaml_VALUE_TOKEN // A VALUE token.
|
||||||
|
|
||||||
|
yaml_ALIAS_TOKEN // An ALIAS token.
|
||||||
|
yaml_ANCHOR_TOKEN // An ANCHOR token.
|
||||||
|
yaml_TAG_TOKEN // A TAG token.
|
||||||
|
yaml_SCALAR_TOKEN // A SCALAR token.
|
||||||
|
)
|
||||||
|
|
||||||
|
func (tt yaml_token_type_t) String() string {
|
||||||
|
switch tt {
|
||||||
|
case yaml_NO_TOKEN:
|
||||||
|
return "yaml_NO_TOKEN"
|
||||||
|
case yaml_STREAM_START_TOKEN:
|
||||||
|
return "yaml_STREAM_START_TOKEN"
|
||||||
|
case yaml_STREAM_END_TOKEN:
|
||||||
|
return "yaml_STREAM_END_TOKEN"
|
||||||
|
case yaml_VERSION_DIRECTIVE_TOKEN:
|
||||||
|
return "yaml_VERSION_DIRECTIVE_TOKEN"
|
||||||
|
case yaml_TAG_DIRECTIVE_TOKEN:
|
||||||
|
return "yaml_TAG_DIRECTIVE_TOKEN"
|
||||||
|
case yaml_DOCUMENT_START_TOKEN:
|
||||||
|
return "yaml_DOCUMENT_START_TOKEN"
|
||||||
|
case yaml_DOCUMENT_END_TOKEN:
|
||||||
|
return "yaml_DOCUMENT_END_TOKEN"
|
||||||
|
case yaml_BLOCK_SEQUENCE_START_TOKEN:
|
||||||
|
return "yaml_BLOCK_SEQUENCE_START_TOKEN"
|
||||||
|
case yaml_BLOCK_MAPPING_START_TOKEN:
|
||||||
|
return "yaml_BLOCK_MAPPING_START_TOKEN"
|
||||||
|
case yaml_BLOCK_END_TOKEN:
|
||||||
|
return "yaml_BLOCK_END_TOKEN"
|
||||||
|
case yaml_FLOW_SEQUENCE_START_TOKEN:
|
||||||
|
return "yaml_FLOW_SEQUENCE_START_TOKEN"
|
||||||
|
case yaml_FLOW_SEQUENCE_END_TOKEN:
|
||||||
|
return "yaml_FLOW_SEQUENCE_END_TOKEN"
|
||||||
|
case yaml_FLOW_MAPPING_START_TOKEN:
|
||||||
|
return "yaml_FLOW_MAPPING_START_TOKEN"
|
||||||
|
case yaml_FLOW_MAPPING_END_TOKEN:
|
||||||
|
return "yaml_FLOW_MAPPING_END_TOKEN"
|
||||||
|
case yaml_BLOCK_ENTRY_TOKEN:
|
||||||
|
return "yaml_BLOCK_ENTRY_TOKEN"
|
||||||
|
case yaml_FLOW_ENTRY_TOKEN:
|
||||||
|
return "yaml_FLOW_ENTRY_TOKEN"
|
||||||
|
case yaml_KEY_TOKEN:
|
||||||
|
return "yaml_KEY_TOKEN"
|
||||||
|
case yaml_VALUE_TOKEN:
|
||||||
|
return "yaml_VALUE_TOKEN"
|
||||||
|
case yaml_ALIAS_TOKEN:
|
||||||
|
return "yaml_ALIAS_TOKEN"
|
||||||
|
case yaml_ANCHOR_TOKEN:
|
||||||
|
return "yaml_ANCHOR_TOKEN"
|
||||||
|
case yaml_TAG_TOKEN:
|
||||||
|
return "yaml_TAG_TOKEN"
|
||||||
|
case yaml_SCALAR_TOKEN:
|
||||||
|
return "yaml_SCALAR_TOKEN"
|
||||||
|
}
|
||||||
|
return "<unknown token>"
|
||||||
|
}
|
||||||
|
|
||||||
|
// The token structure.
|
||||||
|
type yaml_token_t struct {
|
||||||
|
// The token type.
|
||||||
|
typ yaml_token_type_t
|
||||||
|
|
||||||
|
// The start/end of the token.
|
||||||
|
start_mark, end_mark yaml_mark_t
|
||||||
|
|
||||||
|
// The stream encoding (for yaml_STREAM_START_TOKEN).
|
||||||
|
encoding yaml_encoding_t
|
||||||
|
|
||||||
|
// The alias/anchor/scalar value or tag/tag directive handle
|
||||||
|
// (for yaml_ALIAS_TOKEN, yaml_ANCHOR_TOKEN, yaml_SCALAR_TOKEN, yaml_TAG_TOKEN, yaml_TAG_DIRECTIVE_TOKEN).
|
||||||
|
value []byte
|
||||||
|
|
||||||
|
// The tag suffix (for yaml_TAG_TOKEN).
|
||||||
|
suffix []byte
|
||||||
|
|
||||||
|
// The tag directive prefix (for yaml_TAG_DIRECTIVE_TOKEN).
|
||||||
|
prefix []byte
|
||||||
|
|
||||||
|
// The scalar style (for yaml_SCALAR_TOKEN).
|
||||||
|
style yaml_scalar_style_t
|
||||||
|
|
||||||
|
// The version directive major/minor (for yaml_VERSION_DIRECTIVE_TOKEN).
|
||||||
|
major, minor int8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
|
||||||
|
type yaml_event_type_t int8
|
||||||
|
|
||||||
|
// Event types.
|
||||||
|
const (
|
||||||
|
// An empty event.
|
||||||
|
yaml_NO_EVENT yaml_event_type_t = iota
|
||||||
|
|
||||||
|
yaml_STREAM_START_EVENT // A STREAM-START event.
|
||||||
|
yaml_STREAM_END_EVENT // A STREAM-END event.
|
||||||
|
yaml_DOCUMENT_START_EVENT // A DOCUMENT-START event.
|
||||||
|
yaml_DOCUMENT_END_EVENT // A DOCUMENT-END event.
|
||||||
|
yaml_ALIAS_EVENT // An ALIAS event.
|
||||||
|
yaml_SCALAR_EVENT // A SCALAR event.
|
||||||
|
yaml_SEQUENCE_START_EVENT // A SEQUENCE-START event.
|
||||||
|
yaml_SEQUENCE_END_EVENT // A SEQUENCE-END event.
|
||||||
|
yaml_MAPPING_START_EVENT // A MAPPING-START event.
|
||||||
|
yaml_MAPPING_END_EVENT // A MAPPING-END event.
|
||||||
|
yaml_TAIL_COMMENT_EVENT
|
||||||
|
)
|
||||||
|
|
||||||
|
var eventStrings = []string{
|
||||||
|
yaml_NO_EVENT: "none",
|
||||||
|
yaml_STREAM_START_EVENT: "stream start",
|
||||||
|
yaml_STREAM_END_EVENT: "stream end",
|
||||||
|
yaml_DOCUMENT_START_EVENT: "document start",
|
||||||
|
yaml_DOCUMENT_END_EVENT: "document end",
|
||||||
|
yaml_ALIAS_EVENT: "alias",
|
||||||
|
yaml_SCALAR_EVENT: "scalar",
|
||||||
|
yaml_SEQUENCE_START_EVENT: "sequence start",
|
||||||
|
yaml_SEQUENCE_END_EVENT: "sequence end",
|
||||||
|
yaml_MAPPING_START_EVENT: "mapping start",
|
||||||
|
yaml_MAPPING_END_EVENT: "mapping end",
|
||||||
|
yaml_TAIL_COMMENT_EVENT: "tail comment",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e yaml_event_type_t) String() string {
|
||||||
|
if e < 0 || int(e) >= len(eventStrings) {
|
||||||
|
return fmt.Sprintf("unknown event %d", e)
|
||||||
|
}
|
||||||
|
return eventStrings[e]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The event structure.
|
||||||
|
type yaml_event_t struct {
|
||||||
|
|
||||||
|
// The event type.
|
||||||
|
typ yaml_event_type_t
|
||||||
|
|
||||||
|
// The start and end of the event.
|
||||||
|
start_mark, end_mark yaml_mark_t
|
||||||
|
|
||||||
|
// The document encoding (for yaml_STREAM_START_EVENT).
|
||||||
|
encoding yaml_encoding_t
|
||||||
|
|
||||||
|
// The version directive (for yaml_DOCUMENT_START_EVENT).
|
||||||
|
version_directive *yaml_version_directive_t
|
||||||
|
|
||||||
|
// The list of tag directives (for yaml_DOCUMENT_START_EVENT).
|
||||||
|
tag_directives []yaml_tag_directive_t
|
||||||
|
|
||||||
|
// The comments
|
||||||
|
head_comment []byte
|
||||||
|
line_comment []byte
|
||||||
|
foot_comment []byte
|
||||||
|
tail_comment []byte
|
||||||
|
|
||||||
|
// The anchor (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT, yaml_ALIAS_EVENT).
|
||||||
|
anchor []byte
|
||||||
|
|
||||||
|
// The tag (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT).
|
||||||
|
tag []byte
|
||||||
|
|
||||||
|
// The scalar value (for yaml_SCALAR_EVENT).
|
||||||
|
value []byte
|
||||||
|
|
||||||
|
// Is the document start/end indicator implicit, or the tag optional?
|
||||||
|
// (for yaml_DOCUMENT_START_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT, yaml_SCALAR_EVENT).
|
||||||
|
implicit bool
|
||||||
|
|
||||||
|
// Is the tag optional for any non-plain style? (for yaml_SCALAR_EVENT).
|
||||||
|
quoted_implicit bool
|
||||||
|
|
||||||
|
// The style (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT).
|
||||||
|
style yaml_style_t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *yaml_event_t) scalar_style() yaml_scalar_style_t { return yaml_scalar_style_t(e.style) }
|
||||||
|
func (e *yaml_event_t) sequence_style() yaml_sequence_style_t { return yaml_sequence_style_t(e.style) }
|
||||||
|
func (e *yaml_event_t) mapping_style() yaml_mapping_style_t { return yaml_mapping_style_t(e.style) }
|
||||||
|
|
||||||
|
// Nodes
|
||||||
|
|
||||||
|
const (
|
||||||
|
yaml_NULL_TAG = "tag:yaml.org,2002:null" // The tag !!null with the only possible value: null.
|
||||||
|
yaml_BOOL_TAG = "tag:yaml.org,2002:bool" // The tag !!bool with the values: true and false.
|
||||||
|
yaml_STR_TAG = "tag:yaml.org,2002:str" // The tag !!str for string values.
|
||||||
|
yaml_INT_TAG = "tag:yaml.org,2002:int" // The tag !!int for integer values.
|
||||||
|
yaml_FLOAT_TAG = "tag:yaml.org,2002:float" // The tag !!float for float values.
|
||||||
|
yaml_TIMESTAMP_TAG = "tag:yaml.org,2002:timestamp" // The tag !!timestamp for date and time values.
|
||||||
|
|
||||||
|
yaml_SEQ_TAG = "tag:yaml.org,2002:seq" // The tag !!seq is used to denote sequences.
|
||||||
|
yaml_MAP_TAG = "tag:yaml.org,2002:map" // The tag !!map is used to denote mapping.
|
||||||
|
|
||||||
|
// Not in original libyaml.
|
||||||
|
yaml_BINARY_TAG = "tag:yaml.org,2002:binary"
|
||||||
|
yaml_MERGE_TAG = "tag:yaml.org,2002:merge"
|
||||||
|
|
||||||
|
yaml_DEFAULT_SCALAR_TAG = yaml_STR_TAG // The default scalar tag is !!str.
|
||||||
|
yaml_DEFAULT_SEQUENCE_TAG = yaml_SEQ_TAG // The default sequence tag is !!seq.
|
||||||
|
yaml_DEFAULT_MAPPING_TAG = yaml_MAP_TAG // The default mapping tag is !!map.
|
||||||
|
)
|
||||||
|
|
||||||
|
type yaml_node_type_t int
|
||||||
|
|
||||||
|
// Node types.
|
||||||
|
const (
|
||||||
|
// An empty node.
|
||||||
|
yaml_NO_NODE yaml_node_type_t = iota
|
||||||
|
|
||||||
|
yaml_SCALAR_NODE // A scalar node.
|
||||||
|
yaml_SEQUENCE_NODE // A sequence node.
|
||||||
|
yaml_MAPPING_NODE // A mapping node.
|
||||||
|
)
|
||||||
|
|
||||||
|
// An element of a sequence node.
|
||||||
|
type yaml_node_item_t int
|
||||||
|
|
||||||
|
// An element of a mapping node.
|
||||||
|
type yaml_node_pair_t struct {
|
||||||
|
key int // The key of the element.
|
||||||
|
value int // The value of the element.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The node structure.
|
||||||
|
type yaml_node_t struct {
|
||||||
|
typ yaml_node_type_t // The node type.
|
||||||
|
tag []byte // The node tag.
|
||||||
|
|
||||||
|
// The node data.
|
||||||
|
|
||||||
|
// The scalar parameters (for yaml_SCALAR_NODE).
|
||||||
|
scalar struct {
|
||||||
|
value []byte // The scalar value.
|
||||||
|
length int // The length of the scalar value.
|
||||||
|
style yaml_scalar_style_t // The scalar style.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sequence parameters (for YAML_SEQUENCE_NODE).
|
||||||
|
sequence struct {
|
||||||
|
items_data []yaml_node_item_t // The stack of sequence items.
|
||||||
|
style yaml_sequence_style_t // The sequence style.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mapping parameters (for yaml_MAPPING_NODE).
|
||||||
|
mapping struct {
|
||||||
|
pairs_data []yaml_node_pair_t // The stack of mapping pairs (key, value).
|
||||||
|
pairs_start *yaml_node_pair_t // The beginning of the stack.
|
||||||
|
pairs_end *yaml_node_pair_t // The end of the stack.
|
||||||
|
pairs_top *yaml_node_pair_t // The top of the stack.
|
||||||
|
style yaml_mapping_style_t // The mapping style.
|
||||||
|
}
|
||||||
|
|
||||||
|
start_mark yaml_mark_t // The beginning of the node.
|
||||||
|
end_mark yaml_mark_t // The end of the node.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// The document structure.
|
||||||
|
type yaml_document_t struct {
|
||||||
|
|
||||||
|
// The document nodes.
|
||||||
|
nodes []yaml_node_t
|
||||||
|
|
||||||
|
// The version directive.
|
||||||
|
version_directive *yaml_version_directive_t
|
||||||
|
|
||||||
|
// The list of tag directives.
|
||||||
|
tag_directives_data []yaml_tag_directive_t
|
||||||
|
tag_directives_start int // The beginning of the tag directives list.
|
||||||
|
tag_directives_end int // The end of the tag directives list.
|
||||||
|
|
||||||
|
start_implicit int // Is the document start indicator implicit?
|
||||||
|
end_implicit int // Is the document end indicator implicit?
|
||||||
|
|
||||||
|
// The start/end of the document.
|
||||||
|
start_mark, end_mark yaml_mark_t
|
||||||
|
}
|
||||||
|
|
||||||
|
// The prototype of a read handler.
|
||||||
|
//
|
||||||
|
// The read handler is called when the parser needs to read more bytes from the
|
||||||
|
// source. The handler should write not more than size bytes to the buffer.
|
||||||
|
// The number of written bytes should be set to the size_read variable.
|
||||||
|
//
|
||||||
|
// [in,out] data A pointer to an application data specified by
|
||||||
|
//
|
||||||
|
// yaml_parser_set_input().
|
||||||
|
//
|
||||||
|
// [out] buffer The buffer to write the data from the source.
|
||||||
|
// [in] size The size of the buffer.
|
||||||
|
// [out] size_read The actual number of bytes read from the source.
|
||||||
|
//
|
||||||
|
// On success, the handler should return 1. If the handler failed,
|
||||||
|
// the returned value should be 0. On EOF, the handler should set the
|
||||||
|
// size_read to 0 and return 1.
|
||||||
|
type yaml_read_handler_t func(parser *yaml_parser_t, buffer []byte) (n int, err error)
|
||||||
|
|
||||||
|
// This structure holds information about a potential simple key.
|
||||||
|
type yaml_simple_key_t struct {
|
||||||
|
possible bool // Is a simple key possible?
|
||||||
|
required bool // Is a simple key required?
|
||||||
|
token_number int // The number of the token.
|
||||||
|
mark yaml_mark_t // The position mark.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The states of the parser.
|
||||||
|
type yaml_parser_state_t int
|
||||||
|
|
||||||
|
const (
|
||||||
|
yaml_PARSE_STREAM_START_STATE yaml_parser_state_t = iota
|
||||||
|
|
||||||
|
yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE // Expect the beginning of an implicit document.
|
||||||
|
yaml_PARSE_DOCUMENT_START_STATE // Expect DOCUMENT-START.
|
||||||
|
yaml_PARSE_DOCUMENT_CONTENT_STATE // Expect the content of a document.
|
||||||
|
yaml_PARSE_DOCUMENT_END_STATE // Expect DOCUMENT-END.
|
||||||
|
yaml_PARSE_BLOCK_NODE_STATE // Expect a block node.
|
||||||
|
yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE // Expect a block node or indentless sequence.
|
||||||
|
yaml_PARSE_FLOW_NODE_STATE // Expect a flow node.
|
||||||
|
yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a block sequence.
|
||||||
|
yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE // Expect an entry of a block sequence.
|
||||||
|
yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE // Expect an entry of an indentless sequence.
|
||||||
|
yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping.
|
||||||
|
yaml_PARSE_BLOCK_MAPPING_KEY_STATE // Expect a block mapping key.
|
||||||
|
yaml_PARSE_BLOCK_MAPPING_VALUE_STATE // Expect a block mapping value.
|
||||||
|
yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a flow sequence.
|
||||||
|
yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE // Expect an entry of a flow sequence.
|
||||||
|
yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE // Expect a key of an ordered mapping.
|
||||||
|
yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE // Expect a value of an ordered mapping.
|
||||||
|
yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE // Expect the and of an ordered mapping entry.
|
||||||
|
yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping.
|
||||||
|
yaml_PARSE_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping.
|
||||||
|
yaml_PARSE_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping.
|
||||||
|
yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE // Expect an empty value of a flow mapping.
|
||||||
|
yaml_PARSE_END_STATE // Expect nothing.
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ps yaml_parser_state_t) String() string {
|
||||||
|
switch ps {
|
||||||
|
case yaml_PARSE_STREAM_START_STATE:
|
||||||
|
return "yaml_PARSE_STREAM_START_STATE"
|
||||||
|
case yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE:
|
||||||
|
return "yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE"
|
||||||
|
case yaml_PARSE_DOCUMENT_START_STATE:
|
||||||
|
return "yaml_PARSE_DOCUMENT_START_STATE"
|
||||||
|
case yaml_PARSE_DOCUMENT_CONTENT_STATE:
|
||||||
|
return "yaml_PARSE_DOCUMENT_CONTENT_STATE"
|
||||||
|
case yaml_PARSE_DOCUMENT_END_STATE:
|
||||||
|
return "yaml_PARSE_DOCUMENT_END_STATE"
|
||||||
|
case yaml_PARSE_BLOCK_NODE_STATE:
|
||||||
|
return "yaml_PARSE_BLOCK_NODE_STATE"
|
||||||
|
case yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE:
|
||||||
|
return "yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE"
|
||||||
|
case yaml_PARSE_FLOW_NODE_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_NODE_STATE"
|
||||||
|
case yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE:
|
||||||
|
return "yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE"
|
||||||
|
case yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE:
|
||||||
|
return "yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE"
|
||||||
|
case yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE:
|
||||||
|
return "yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE"
|
||||||
|
case yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE:
|
||||||
|
return "yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE"
|
||||||
|
case yaml_PARSE_BLOCK_MAPPING_KEY_STATE:
|
||||||
|
return "yaml_PARSE_BLOCK_MAPPING_KEY_STATE"
|
||||||
|
case yaml_PARSE_BLOCK_MAPPING_VALUE_STATE:
|
||||||
|
return "yaml_PARSE_BLOCK_MAPPING_VALUE_STATE"
|
||||||
|
case yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE"
|
||||||
|
case yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE"
|
||||||
|
case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE"
|
||||||
|
case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE"
|
||||||
|
case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE"
|
||||||
|
case yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE"
|
||||||
|
case yaml_PARSE_FLOW_MAPPING_KEY_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_MAPPING_KEY_STATE"
|
||||||
|
case yaml_PARSE_FLOW_MAPPING_VALUE_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_MAPPING_VALUE_STATE"
|
||||||
|
case yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE:
|
||||||
|
return "yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE"
|
||||||
|
case yaml_PARSE_END_STATE:
|
||||||
|
return "yaml_PARSE_END_STATE"
|
||||||
|
}
|
||||||
|
return "<unknown parser state>"
|
||||||
|
}
|
||||||
|
|
||||||
|
// This structure holds aliases data.
|
||||||
|
type yaml_alias_data_t struct {
|
||||||
|
anchor []byte // The anchor.
|
||||||
|
index int // The node id.
|
||||||
|
mark yaml_mark_t // The anchor mark.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The parser structure.
|
||||||
|
//
|
||||||
|
// All members are internal. Manage the structure using the
|
||||||
|
// yaml_parser_ family of functions.
|
||||||
|
type yaml_parser_t struct {
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
|
||||||
|
error yaml_error_type_t // Error type.
|
||||||
|
|
||||||
|
problem string // Error description.
|
||||||
|
|
||||||
|
// The byte about which the problem occurred.
|
||||||
|
problem_offset int
|
||||||
|
problem_value int
|
||||||
|
problem_mark yaml_mark_t
|
||||||
|
|
||||||
|
// The error context.
|
||||||
|
context string
|
||||||
|
context_mark yaml_mark_t
|
||||||
|
|
||||||
|
// Reader stuff
|
||||||
|
|
||||||
|
read_handler yaml_read_handler_t // Read handler.
|
||||||
|
|
||||||
|
input_reader io.Reader // File input data.
|
||||||
|
input []byte // String input data.
|
||||||
|
input_pos int
|
||||||
|
|
||||||
|
eof bool // EOF flag
|
||||||
|
|
||||||
|
buffer []byte // The working buffer.
|
||||||
|
buffer_pos int // The current position of the buffer.
|
||||||
|
|
||||||
|
unread int // The number of unread characters in the buffer.
|
||||||
|
|
||||||
|
newlines int // The number of line breaks since last non-break/non-blank character
|
||||||
|
|
||||||
|
raw_buffer []byte // The raw buffer.
|
||||||
|
raw_buffer_pos int // The current position of the buffer.
|
||||||
|
|
||||||
|
encoding yaml_encoding_t // The input encoding.
|
||||||
|
|
||||||
|
offset int // The offset of the current position (in bytes).
|
||||||
|
mark yaml_mark_t // The mark of the current position.
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
|
||||||
|
head_comment []byte // The current head comments
|
||||||
|
line_comment []byte // The current line comments
|
||||||
|
foot_comment []byte // The current foot comments
|
||||||
|
tail_comment []byte // Foot comment that happens at the end of a block.
|
||||||
|
stem_comment []byte // Comment in item preceding a nested structure (list inside list item, etc)
|
||||||
|
|
||||||
|
comments []yaml_comment_t // The folded comments for all parsed tokens
|
||||||
|
comments_head int
|
||||||
|
|
||||||
|
// Scanner stuff
|
||||||
|
|
||||||
|
stream_start_produced bool // Have we started to scan the input stream?
|
||||||
|
stream_end_produced bool // Have we reached the end of the input stream?
|
||||||
|
|
||||||
|
flow_level int // The number of unclosed '[' and '{' indicators.
|
||||||
|
|
||||||
|
tokens []yaml_token_t // The tokens queue.
|
||||||
|
tokens_head int // The head of the tokens queue.
|
||||||
|
tokens_parsed int // The number of tokens fetched from the queue.
|
||||||
|
token_available bool // Does the tokens queue contain a token ready for dequeueing.
|
||||||
|
|
||||||
|
indent int // The current indentation level.
|
||||||
|
indents []int // The indentation levels stack.
|
||||||
|
|
||||||
|
simple_key_allowed bool // May a simple key occur at the current position?
|
||||||
|
simple_keys []yaml_simple_key_t // The stack of simple keys.
|
||||||
|
simple_keys_by_tok map[int]int // possible simple_key indexes indexed by token_number
|
||||||
|
|
||||||
|
// Parser stuff
|
||||||
|
|
||||||
|
state yaml_parser_state_t // The current parser state.
|
||||||
|
states []yaml_parser_state_t // The parser states stack.
|
||||||
|
marks []yaml_mark_t // The stack of marks.
|
||||||
|
tag_directives []yaml_tag_directive_t // The list of TAG directives.
|
||||||
|
|
||||||
|
// Dumper stuff
|
||||||
|
|
||||||
|
aliases []yaml_alias_data_t // The alias data.
|
||||||
|
|
||||||
|
document *yaml_document_t // The currently parsed document.
|
||||||
|
}
|
||||||
|
|
||||||
|
type yaml_comment_t struct {
|
||||||
|
scan_mark yaml_mark_t // Position where scanning for comments started
|
||||||
|
token_mark yaml_mark_t // Position after which tokens will be associated with this comment
|
||||||
|
start_mark yaml_mark_t // Position of '#' comment mark
|
||||||
|
end_mark yaml_mark_t // Position where comment terminated
|
||||||
|
|
||||||
|
head []byte
|
||||||
|
line []byte
|
||||||
|
foot []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emitter Definitions
|
||||||
|
|
||||||
|
// The prototype of a write handler.
|
||||||
|
//
|
||||||
|
// The write handler is called when the emitter needs to flush the accumulated
|
||||||
|
// characters to the output. The handler should write @a size bytes of the
|
||||||
|
// @a buffer to the output.
|
||||||
|
//
|
||||||
|
// @param[in,out] data A pointer to an application data specified by
|
||||||
|
//
|
||||||
|
// yaml_emitter_set_output().
|
||||||
|
//
|
||||||
|
// @param[in] buffer The buffer with bytes to be written.
|
||||||
|
// @param[in] size The size of the buffer.
|
||||||
|
//
|
||||||
|
// @returns On success, the handler should return @c 1. If the handler failed,
|
||||||
|
// the returned value should be @c 0.
|
||||||
|
type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error
|
||||||
|
|
||||||
|
type yaml_emitter_state_t int
|
||||||
|
|
||||||
|
// The emitter states.
|
||||||
|
const (
|
||||||
|
// Expect STREAM-START.
|
||||||
|
yaml_EMIT_STREAM_START_STATE yaml_emitter_state_t = iota
|
||||||
|
|
||||||
|
yaml_EMIT_FIRST_DOCUMENT_START_STATE // Expect the first DOCUMENT-START or STREAM-END.
|
||||||
|
yaml_EMIT_DOCUMENT_START_STATE // Expect DOCUMENT-START or STREAM-END.
|
||||||
|
yaml_EMIT_DOCUMENT_CONTENT_STATE // Expect the content of a document.
|
||||||
|
yaml_EMIT_DOCUMENT_END_STATE // Expect DOCUMENT-END.
|
||||||
|
yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a flow sequence.
|
||||||
|
yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE // Expect the next item of a flow sequence, with the comma already written out
|
||||||
|
yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE // Expect an item of a flow sequence.
|
||||||
|
yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping.
|
||||||
|
yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE // Expect the next key of a flow mapping, with the comma already written out
|
||||||
|
yaml_EMIT_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping.
|
||||||
|
yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a flow mapping.
|
||||||
|
yaml_EMIT_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping.
|
||||||
|
yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a block sequence.
|
||||||
|
yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE // Expect an item of a block sequence.
|
||||||
|
yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping.
|
||||||
|
yaml_EMIT_BLOCK_MAPPING_KEY_STATE // Expect the key of a block mapping.
|
||||||
|
yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a block mapping.
|
||||||
|
yaml_EMIT_BLOCK_MAPPING_VALUE_STATE // Expect a value of a block mapping.
|
||||||
|
yaml_EMIT_END_STATE // Expect nothing.
|
||||||
|
)
|
||||||
|
|
||||||
|
// The emitter structure.
|
||||||
|
//
|
||||||
|
// All members are internal. Manage the structure using the @c yaml_emitter_
|
||||||
|
// family of functions.
|
||||||
|
type yaml_emitter_t struct {
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
|
||||||
|
error yaml_error_type_t // Error type.
|
||||||
|
problem string // Error description.
|
||||||
|
|
||||||
|
// Writer stuff
|
||||||
|
|
||||||
|
write_handler yaml_write_handler_t // Write handler.
|
||||||
|
|
||||||
|
output_buffer *[]byte // String output data.
|
||||||
|
output_writer io.Writer // File output data.
|
||||||
|
|
||||||
|
buffer []byte // The working buffer.
|
||||||
|
buffer_pos int // The current position of the buffer.
|
||||||
|
|
||||||
|
raw_buffer []byte // The raw buffer.
|
||||||
|
raw_buffer_pos int // The current position of the buffer.
|
||||||
|
|
||||||
|
encoding yaml_encoding_t // The stream encoding.
|
||||||
|
|
||||||
|
// Emitter stuff
|
||||||
|
|
||||||
|
canonical bool // If the output is in the canonical style?
|
||||||
|
best_indent int // The number of indentation spaces.
|
||||||
|
best_width int // The preferred width of the output lines.
|
||||||
|
unicode bool // Allow unescaped non-ASCII characters?
|
||||||
|
line_break yaml_break_t // The preferred line break.
|
||||||
|
|
||||||
|
state yaml_emitter_state_t // The current emitter state.
|
||||||
|
states []yaml_emitter_state_t // The stack of states.
|
||||||
|
|
||||||
|
events []yaml_event_t // The event queue.
|
||||||
|
events_head int // The head of the event queue.
|
||||||
|
|
||||||
|
indents []int // The stack of indentation levels.
|
||||||
|
|
||||||
|
tag_directives []yaml_tag_directive_t // The list of tag directives.
|
||||||
|
|
||||||
|
indent int // The current indentation level.
|
||||||
|
|
||||||
|
flow_level int // The current flow level.
|
||||||
|
|
||||||
|
root_context bool // Is it the document root context?
|
||||||
|
sequence_context bool // Is it a sequence context?
|
||||||
|
mapping_context bool // Is it a mapping context?
|
||||||
|
simple_key_context bool // Is it a simple mapping key context?
|
||||||
|
|
||||||
|
line int // The current line.
|
||||||
|
column int // The current column.
|
||||||
|
whitespace bool // If the last character was a whitespace?
|
||||||
|
indention bool // If the last character was an indentation character (' ', '-', '?', ':')?
|
||||||
|
open_ended bool // If an explicit document end is required?
|
||||||
|
|
||||||
|
space_above bool // Is there's an empty line above?
|
||||||
|
foot_indent int // The indent used to write the foot comment above, or -1 if none.
|
||||||
|
|
||||||
|
// Anchor analysis.
|
||||||
|
anchor_data struct {
|
||||||
|
anchor []byte // The anchor value.
|
||||||
|
alias bool // Is it an alias?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag analysis.
|
||||||
|
tag_data struct {
|
||||||
|
handle []byte // The tag handle.
|
||||||
|
suffix []byte // The tag suffix.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar analysis.
|
||||||
|
scalar_data struct {
|
||||||
|
value []byte // The scalar value.
|
||||||
|
multiline bool // Does the scalar contain line breaks?
|
||||||
|
flow_plain_allowed bool // Can the scalar be expessed in the flow plain style?
|
||||||
|
block_plain_allowed bool // Can the scalar be expressed in the block plain style?
|
||||||
|
single_quoted_allowed bool // Can the scalar be expressed in the single quoted style?
|
||||||
|
block_allowed bool // Can the scalar be expressed in the literal or folded styles?
|
||||||
|
style yaml_scalar_style_t // The output style.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
head_comment []byte
|
||||||
|
line_comment []byte
|
||||||
|
foot_comment []byte
|
||||||
|
tail_comment []byte
|
||||||
|
|
||||||
|
key_line_comment []byte
|
||||||
|
|
||||||
|
// Dumper stuff
|
||||||
|
|
||||||
|
opened bool // If the stream was already opened?
|
||||||
|
closed bool // If the stream was already closed?
|
||||||
|
|
||||||
|
// The information associated with the document nodes.
|
||||||
|
anchors *struct {
|
||||||
|
references int // The number of references.
|
||||||
|
anchor int // The anchor id.
|
||||||
|
serialized bool // If the node has been emitted?
|
||||||
|
}
|
||||||
|
|
||||||
|
last_anchor_id int // The last assigned anchor id.
|
||||||
|
|
||||||
|
document *yaml_document_t // The currently emitted document.
|
||||||
|
}
|
||||||
198
third_party/yaml.v3/yamlprivateh.go
vendored
Normal file
198
third_party/yaml.v3/yamlprivateh.go
vendored
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
// Copyright (c) 2006-2010 Kirill Simonov
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
// so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
const (
|
||||||
|
// The size of the input raw buffer.
|
||||||
|
input_raw_buffer_size = 512
|
||||||
|
|
||||||
|
// The size of the input buffer.
|
||||||
|
// It should be possible to decode the whole raw buffer.
|
||||||
|
input_buffer_size = input_raw_buffer_size * 3
|
||||||
|
|
||||||
|
// The size of the output buffer.
|
||||||
|
output_buffer_size = 128
|
||||||
|
|
||||||
|
// The size of the output raw buffer.
|
||||||
|
// It should be possible to encode the whole output buffer.
|
||||||
|
output_raw_buffer_size = (output_buffer_size*2 + 2)
|
||||||
|
|
||||||
|
// The size of other stacks and queues.
|
||||||
|
initial_stack_size = 16
|
||||||
|
initial_queue_size = 16
|
||||||
|
initial_string_size = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if the character at the specified position is an alphabetical
|
||||||
|
// character, a digit, '_', or '-'.
|
||||||
|
func is_alpha(b []byte, i int) bool {
|
||||||
|
return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character at the specified position is a digit.
|
||||||
|
func is_digit(b []byte, i int) bool {
|
||||||
|
return b[i] >= '0' && b[i] <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value of a digit.
|
||||||
|
func as_digit(b []byte, i int) int {
|
||||||
|
return int(b[i]) - '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character at the specified position is a hex-digit.
|
||||||
|
func is_hex(b []byte, i int) bool {
|
||||||
|
return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || b[i] >= 'a' && b[i] <= 'f'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value of a hex-digit.
|
||||||
|
func as_hex(b []byte, i int) int {
|
||||||
|
bi := b[i]
|
||||||
|
if bi >= 'A' && bi <= 'F' {
|
||||||
|
return int(bi) - 'A' + 10
|
||||||
|
}
|
||||||
|
if bi >= 'a' && bi <= 'f' {
|
||||||
|
return int(bi) - 'a' + 10
|
||||||
|
}
|
||||||
|
return int(bi) - '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character is ASCII.
|
||||||
|
func is_ascii(b []byte, i int) bool {
|
||||||
|
return b[i] <= 0x7F
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character at the start of the buffer can be printed unescaped.
|
||||||
|
func is_printable(b []byte, i int) bool {
|
||||||
|
return ((b[i] == 0x0A) || // . == #x0A
|
||||||
|
(b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E
|
||||||
|
(b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF
|
||||||
|
(b[i] > 0xC2 && b[i] < 0xED) ||
|
||||||
|
(b[i] == 0xED && b[i+1] < 0xA0) ||
|
||||||
|
(b[i] == 0xEE) ||
|
||||||
|
(b[i] == 0xEF && // #xE000 <= . <= #xFFFD
|
||||||
|
!(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF
|
||||||
|
!(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character at the specified position is NUL.
|
||||||
|
func is_z(b []byte, i int) bool {
|
||||||
|
return b[i] == 0x00
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the beginning of the buffer is a BOM.
|
||||||
|
func is_bom(b []byte, i int) bool {
|
||||||
|
return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character at the specified position is space.
|
||||||
|
func is_space(b []byte, i int) bool {
|
||||||
|
return b[i] == ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character at the specified position is tab.
|
||||||
|
func is_tab(b []byte, i int) bool {
|
||||||
|
return b[i] == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character at the specified position is blank (space or tab).
|
||||||
|
func is_blank(b []byte, i int) bool {
|
||||||
|
//return is_space(b, i) || is_tab(b, i)
|
||||||
|
return b[i] == ' ' || b[i] == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character at the specified position is a line break.
|
||||||
|
func is_break(b []byte, i int) bool {
|
||||||
|
return (b[i] == '\r' || // CR (#xD)
|
||||||
|
b[i] == '\n' || // LF (#xA)
|
||||||
|
b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85)
|
||||||
|
b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028)
|
||||||
|
b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029)
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_crlf(b []byte, i int) bool {
|
||||||
|
return b[i] == '\r' && b[i+1] == '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character is a line break or NUL.
|
||||||
|
func is_breakz(b []byte, i int) bool {
|
||||||
|
//return is_break(b, i) || is_z(b, i)
|
||||||
|
return (
|
||||||
|
// is_break:
|
||||||
|
b[i] == '\r' || // CR (#xD)
|
||||||
|
b[i] == '\n' || // LF (#xA)
|
||||||
|
b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85)
|
||||||
|
b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028)
|
||||||
|
b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029)
|
||||||
|
// is_z:
|
||||||
|
b[i] == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character is a line break, space, or NUL.
|
||||||
|
func is_spacez(b []byte, i int) bool {
|
||||||
|
//return is_space(b, i) || is_breakz(b, i)
|
||||||
|
return (
|
||||||
|
// is_space:
|
||||||
|
b[i] == ' ' ||
|
||||||
|
// is_breakz:
|
||||||
|
b[i] == '\r' || // CR (#xD)
|
||||||
|
b[i] == '\n' || // LF (#xA)
|
||||||
|
b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85)
|
||||||
|
b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028)
|
||||||
|
b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029)
|
||||||
|
b[i] == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the character is a line break, space, tab, or NUL.
|
||||||
|
func is_blankz(b []byte, i int) bool {
|
||||||
|
//return is_blank(b, i) || is_breakz(b, i)
|
||||||
|
return (
|
||||||
|
// is_blank:
|
||||||
|
b[i] == ' ' || b[i] == '\t' ||
|
||||||
|
// is_breakz:
|
||||||
|
b[i] == '\r' || // CR (#xD)
|
||||||
|
b[i] == '\n' || // LF (#xA)
|
||||||
|
b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85)
|
||||||
|
b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028)
|
||||||
|
b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029)
|
||||||
|
b[i] == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the width of the character.
|
||||||
|
func width(b byte) int {
|
||||||
|
// Don't replace these by a switch without first
|
||||||
|
// confirming that it is being inlined.
|
||||||
|
if b&0x80 == 0x00 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if b&0xE0 == 0xC0 {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if b&0xF0 == 0xE0 {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
if b&0xF8 == 0xF0 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
|
||||||
|
}
|
||||||
BIN
tonk
BIN
tonk
Binary file not shown.
|
|
@ -307,10 +307,13 @@ func init() {
|
||||||
"game.corpsestarch.name": {"Трупные батончики (Corpse-Starch Box)", "Corpse-Starch Box"},
|
"game.corpsestarch.name": {"Трупные батончики (Corpse-Starch Box)", "Corpse-Starch Box"},
|
||||||
"game.corpsestarch.desc": {"Инкрементальная игра: копите трупный крахмал, открывайте новые механики по ходу партии", "An incremental game: gather corpse starch, unlock new mechanics as you go"},
|
"game.corpsestarch.desc": {"Инкрементальная игра: копите трупный крахмал, открывайте новые механики по ходу партии", "An incremental game: gather corpse starch, unlock new mechanics as you go"},
|
||||||
|
|
||||||
"corpsestarch.title": {"=== ТРУПНЫЕ БАТОНЧИКИ ===", "=== CORPSE-STARCH BOX ==="},
|
"corpsestarch.title": {"=== ТРУПНЫЕ БАТОНЧИКИ ===", "=== CORPSE-STARCH BOX ==="},
|
||||||
"corpsestarch.header.starch": {"Трупный крахмал: %s (всего добыто: %s)", "Corpse starch: %s (total gathered: %s)"},
|
"corpsestarch.header.starch": {"Трупный крахмал: %s (всего добыто: %s)", "Corpse starch: %s (total gathered: %s)"},
|
||||||
"corpsestarch.header.corruption": {"Скверна: %d", "Corruption: %d"},
|
"corpsestarch.header.corruption": {"Скверна: %d", "Corruption: %d"},
|
||||||
"corpsestarch.hint": {"[1-9] выбрать действие [q] в меню (партия автосохраняется)", "[1-9] choose an action [q] menu (game is autosaved)"},
|
"corpsestarch.hint": {"[1-9] выбрать действие [N] начать заново [q] в меню (партия автосохраняется)", "[1-9] choose an action [N] start over [q] menu (game is autosaved)"},
|
||||||
|
"corpsestarch.confirm_reset": {"Вы уверены? Весь прогресс будет безвозвратно потерян.", "Are you sure? All progress will be permanently lost."},
|
||||||
|
"corpsestarch.confirm_reset.hint": {"[y] да, начать заново [n/esc] отмена", "[y] yes, start over [n/esc] cancel"},
|
||||||
|
"corpsestarch.msg.reset_done": {"Начата новая партия.", "A new game has started."},
|
||||||
|
|
||||||
"corpsestarch.action.requisition": {"Реквизировать трупный крахмал (+%s)", "Requisition corpse starch (+%s)"},
|
"corpsestarch.action.requisition": {"Реквизировать трупный крахмал (+%s)", "Requisition corpse starch (+%s)"},
|
||||||
"corpsestarch.action.buy_servitor": {"Приобрести: %s (сейчас: %d, цена: %s)", "Acquire: %s (owned: %d, cost: %s)"},
|
"corpsestarch.action.buy_servitor": {"Приобрести: %s (сейчас: %d, цена: %s)", "Acquire: %s (owned: %d, cost: %s)"},
|
||||||
|
|
@ -365,6 +368,7 @@ func init() {
|
||||||
"go.hint.play": {"[↑↓←→/hjkl] курсор [enter] поставить камень [p] пасовать [q] в меню", "[↑↓←→/hjkl] cursor [enter] place a stone [p] pass [q] menu"},
|
"go.hint.play": {"[↑↓←→/hjkl] курсор [enter] поставить камень [p] пасовать [q] в меню", "[↑↓←→/hjkl] cursor [enter] place a stone [p] pass [q] menu"},
|
||||||
"go.hint.gameover": {"[n] новая партия [q] в меню", "[n] new game [q] menu"},
|
"go.hint.gameover": {"[n] новая партия [q] в меню", "[n] new game [q] menu"},
|
||||||
"go.msg.bot_moved": {"Бот сходил.", "The bot made a move."},
|
"go.msg.bot_moved": {"Бот сходил.", "The bot made a move."},
|
||||||
|
"go.msg.bot_passed": {"Бот пасовал.", "The bot passed."},
|
||||||
"go.msg.played": {"Камень поставлен.", "Stone placed."},
|
"go.msg.played": {"Камень поставлен.", "Stone placed."},
|
||||||
"go.msg.passed": {"Вы пасовали.", "You passed."},
|
"go.msg.passed": {"Вы пасовали.", "You passed."},
|
||||||
"go.result.line": {"Партия окончена! Победили %s (счёт: %.1f — %.1f)", "Game over! %s win (score: %.1f — %.1f)"},
|
"go.result.line": {"Партия окончена! Победили %s (счёт: %.1f — %.1f)", "Game over! %s win (score: %.1f — %.1f)"},
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ package main
|
||||||
// translations_wayfarer1/2/3.go.
|
// translations_wayfarer1/2/3.go.
|
||||||
func init() {
|
func init() {
|
||||||
for k, v := range map[string]translationEntry{
|
for k, v := range map[string]translationEntry{
|
||||||
"wf.title": {"=== СТРАННИК ===", "=== WAYFARER ==="},
|
"wf.title": {"=== СТРАННИК ===", "=== WAYFARER ==="},
|
||||||
|
"wf.confirm_reset": {"Вы уверены? Текущая карьера будет безвозвратно потеряна.", "Are you sure? Your current career will be permanently lost."},
|
||||||
|
"wf.confirm_reset.hint": {"[y] да, начать новую карьеру [n/esc] отмена", "[y] yes, start a new career [n/esc] cancel"},
|
||||||
|
"wf.msg.reset_done": {"Начата новая карьера.", "A new career has started."},
|
||||||
|
|
||||||
"wf.status_line": {
|
"wf.status_line": {
|
||||||
"Кредиты: %s Груз: %d/%d Топливо: %.1f/%.1f Корпус: %d/%d Щиты: %d/%d",
|
"Кредиты: %s Груз: %d/%d Топливо: %.1f/%.1f Корпус: %d/%d Щиты: %d/%d",
|
||||||
|
|
@ -17,7 +20,7 @@ func init() {
|
||||||
|
|
||||||
"wf.hub.hint1": {"[1] Рынок [2] Верфь [3] Перелёт [4] Задания", "[1] Market [2] Shipyard [3] Travel [4] Quests"},
|
"wf.hub.hint1": {"[1] Рынок [2] Верфь [3] Перелёт [4] Задания", "[1] Market [2] Shipyard [3] Travel [4] Quests"},
|
||||||
"wf.hub.hint2": {"Пристыкованы к станции.", "Docked at the station."},
|
"wf.hub.hint2": {"Пристыкованы к станции.", "Docked at the station."},
|
||||||
"wf.hint.quit": {"[q] сохранить и выйти в меню", "[q] save and quit to menu"},
|
"wf.hint.quit": {"[q] сохранить и выйти в меню [N] начать новую карьеру", "[q] save and quit to menu [N] start a new career"},
|
||||||
|
|
||||||
"wf.market.title": {"=== РЫНОК ===", "=== MARKET ==="},
|
"wf.market.title": {"=== РЫНОК ===", "=== MARKET ==="},
|
||||||
"wf.market.line": {"%-22s цена: %8.1f в трюме: %-3d", "%-22s price: %8.1f held: %-3d"},
|
"wf.market.line": {"%-22s цена: %8.1f в трюме: %-3d", "%-22s price: %8.1f held: %-3d"},
|
||||||
|
|
|
||||||
231
wayfarer_quest_loader.go
Normal file
231
wayfarer_quest_loader.go
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ПРИМЕЧАНИЕ: помимо 8 встроенных квестов (см. wayfarer_quests.go),
|
||||||
|
// можно добавлять собственные квесты, не трогая код коллекции —
|
||||||
|
// достаточно положить .yaml файл в директорию "quests" рядом с
|
||||||
|
// исполняемым файлом (или в текущей рабочей директории). Формат
|
||||||
|
// файла и полное описание — в rules.go (правила Странника) и в
|
||||||
|
// wayfarer_quest_loader_example.yaml.
|
||||||
|
//
|
||||||
|
// Загрузка максимально терпима к ошибкам: файл с некорректным
|
||||||
|
// синтаксисом, отсутствующими полями или неполным переводом просто
|
||||||
|
// пропускается с предупреждением в stderr — никогда не приводит к
|
||||||
|
// падению всей программы. Ключ квеста, уже занятый одним из
|
||||||
|
// встроенных (или другим уже загруженным файлом), тоже пропускается
|
||||||
|
// с предупреждением, а не молча перезаписывает существующий.
|
||||||
|
|
||||||
|
// wfLocalizedText — пара текстов на русском и английском в одном
|
||||||
|
// YAML-поле.
|
||||||
|
type wfLocalizedText struct {
|
||||||
|
RU string `yaml:"ru"`
|
||||||
|
EN string `yaml:"en"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wayfarerQuestChoiceYAML struct {
|
||||||
|
Label wfLocalizedText `yaml:"label"`
|
||||||
|
NextStep *int `yaml:"next_step"`
|
||||||
|
Terminal bool `yaml:"terminal"`
|
||||||
|
Success bool `yaml:"success"`
|
||||||
|
Result wfLocalizedText `yaml:"result"`
|
||||||
|
CreditsDelta float64 `yaml:"credits_delta"`
|
||||||
|
HullDelta int `yaml:"hull_delta"`
|
||||||
|
CargoIdx *int `yaml:"cargo_idx"`
|
||||||
|
CargoDelta int `yaml:"cargo_delta"`
|
||||||
|
KillsDelta int `yaml:"kills_delta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wayfarerQuestStepYAML struct {
|
||||||
|
Text wfLocalizedText `yaml:"text"`
|
||||||
|
Choices []wayfarerQuestChoiceYAML `yaml:"choices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wayfarerQuestYAML struct {
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
Title wfLocalizedText `yaml:"title"`
|
||||||
|
Brief wfLocalizedText `yaml:"brief"`
|
||||||
|
Steps []wayfarerQuestStepYAML `yaml:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// wfLoadedText — один текст для регистрации в общей карте переводов.
|
||||||
|
type wfLoadedText struct {
|
||||||
|
key, ru, en string
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertYAMLQuest превращает разобранный YAML в WayfarerQuestDef и
|
||||||
|
// список текстов для регистрации переводов. sourceName — имя файла,
|
||||||
|
// только для сообщений об ошибках.
|
||||||
|
func convertYAMLQuest(y wayfarerQuestYAML, sourceName string) (WayfarerQuestDef, []wfLoadedText, error) {
|
||||||
|
var def WayfarerQuestDef
|
||||||
|
|
||||||
|
if y.Key == "" {
|
||||||
|
return def, nil, fmt.Errorf("%s: не указано поле key", sourceName)
|
||||||
|
}
|
||||||
|
if len(y.Steps) == 0 {
|
||||||
|
return def, nil, fmt.Errorf("%s: квест %q — нет ни одного шага (steps)", sourceName, y.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
def.Key = y.Key
|
||||||
|
def.TitleKey = y.Key + ".title"
|
||||||
|
def.BriefKey = y.Key + ".brief"
|
||||||
|
|
||||||
|
var texts []wfLoadedText
|
||||||
|
texts = append(texts,
|
||||||
|
wfLoadedText{def.TitleKey, y.Title.RU, y.Title.EN},
|
||||||
|
wfLoadedText{def.BriefKey, y.Brief.RU, y.Brief.EN},
|
||||||
|
)
|
||||||
|
|
||||||
|
for si, step := range y.Steps {
|
||||||
|
stepTextKey := fmt.Sprintf("%s.step%d", y.Key, si)
|
||||||
|
texts = append(texts, wfLoadedText{stepTextKey, step.Text.RU, step.Text.EN})
|
||||||
|
|
||||||
|
if len(step.Choices) == 0 {
|
||||||
|
return def, nil, fmt.Errorf("%s: квест %q, шаг %d — нет вариантов выбора (choices)", sourceName, y.Key, si)
|
||||||
|
}
|
||||||
|
|
||||||
|
stepOut := WayfarerQuestStep{TextKey: stepTextKey}
|
||||||
|
for ci, choice := range step.Choices {
|
||||||
|
labelKey := fmt.Sprintf("%s.choice%d.%d", y.Key, si, ci)
|
||||||
|
texts = append(texts, wfLoadedText{labelKey, choice.Label.RU, choice.Label.EN})
|
||||||
|
|
||||||
|
choiceOut := WayfarerQuestChoice{
|
||||||
|
LabelKey: labelKey,
|
||||||
|
Terminal: choice.Terminal,
|
||||||
|
Success: choice.Success,
|
||||||
|
CreditsDelta: choice.CreditsDelta,
|
||||||
|
HullDelta: choice.HullDelta,
|
||||||
|
CargoDelta: choice.CargoDelta,
|
||||||
|
KillsDelta: choice.KillsDelta,
|
||||||
|
CargoIdx: -1,
|
||||||
|
}
|
||||||
|
if choice.CargoIdx != nil {
|
||||||
|
choiceOut.CargoIdx = *choice.CargoIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
if choice.Terminal {
|
||||||
|
resultKey := fmt.Sprintf("%s.result%d.%d", y.Key, si, ci)
|
||||||
|
texts = append(texts, wfLoadedText{resultKey, choice.Result.RU, choice.Result.EN})
|
||||||
|
choiceOut.ResultKey = resultKey
|
||||||
|
} else {
|
||||||
|
if choice.NextStep == nil {
|
||||||
|
return def, nil, fmt.Errorf("%s: квест %q, шаг %d, выбор %d — не указан next_step (обязателен, если это не конец квеста)",
|
||||||
|
sourceName, y.Key, si, ci)
|
||||||
|
}
|
||||||
|
if *choice.NextStep < 0 || *choice.NextStep >= len(y.Steps) {
|
||||||
|
return def, nil, fmt.Errorf("%s: квест %q, шаг %d, выбор %d — next_step %d вне диапазона шагов (0..%d)",
|
||||||
|
sourceName, y.Key, si, ci, *choice.NextStep, len(y.Steps)-1)
|
||||||
|
}
|
||||||
|
choiceOut.NextStep = *choice.NextStep
|
||||||
|
}
|
||||||
|
stepOut.Choices = append(stepOut.Choices, choiceOut)
|
||||||
|
}
|
||||||
|
def.Steps = append(def.Steps, stepOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
return def, texts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadWayfarerQuestsFromDir читает все *.yaml/*.yml файлы в dir и
|
||||||
|
// возвращает успешно разобранные квесты и список предупреждений по
|
||||||
|
// пропущенным файлам (пустая/отсутствующая директория — не ошибка,
|
||||||
|
// просто нет пользовательских квестов). existingKeys — ключи, уже
|
||||||
|
// занятые (встроенными или ранее загруженными квестами); функция не
|
||||||
|
// мутирует эту карту.
|
||||||
|
func LoadWayfarerQuestsFromDir(dir string, existingKeys map[string]bool) ([]WayfarerQuestDef, []string) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
taken := make(map[string]bool, len(existingKeys))
|
||||||
|
for k := range existingKeys {
|
||||||
|
taken[k] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var loaded []WayfarerQuestDef
|
||||||
|
var warnings []string
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if !strings.HasSuffix(name, ".yaml") && !strings.HasSuffix(name, ".yml") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, name)
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("%s: не удалось прочитать файл: %v", name, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var y wayfarerQuestYAML
|
||||||
|
if err := yaml.Unmarshal(data, &y); err != nil {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("%s: ошибка разбора YAML: %v", name, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
def, texts, err := convertYAMLQuest(y, name)
|
||||||
|
if err != nil {
|
||||||
|
warnings = append(warnings, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if taken[def.Key] {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("%s: квест с ключом %q уже занят, файл пропущен", name, def.Key))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
incomplete := false
|
||||||
|
for _, tx := range texts {
|
||||||
|
if tx.ru == "" || tx.en == "" {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("%s: у ключа %q не заполнен перевод (ru=%q en=%q)", name, tx.key, tx.ru, tx.en))
|
||||||
|
incomplete = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if incomplete {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tx := range texts {
|
||||||
|
translations[tx.key] = translationEntry{ru: tx.ru, en: tx.en}
|
||||||
|
}
|
||||||
|
loaded = append(loaded, def)
|
||||||
|
taken[def.Key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return loaded, warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadCustomWayfarerQuests ищет пользовательские квесты в директории
|
||||||
|
// "quests" рядом с исполняемым файлом и в текущей рабочей директории
|
||||||
|
// (та же логика поиска, что и у .env для сида галактики), добавляя
|
||||||
|
// найденные к WayfarerQuestDefs. Предупреждения о пропущенных файлах
|
||||||
|
// выводятся в stderr, не мешая TUI.
|
||||||
|
func loadCustomWayfarerQuests() {
|
||||||
|
existingKeys := make(map[string]bool, len(WayfarerQuestDefs))
|
||||||
|
for _, q := range WayfarerQuestDefs {
|
||||||
|
existingKeys[q.Key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range wayfarerEnvSearchDirs() {
|
||||||
|
questsDir := filepath.Join(dir, "quests")
|
||||||
|
loaded, warnings := LoadWayfarerQuestsFromDir(questsDir, existingKeys)
|
||||||
|
for _, w := range warnings {
|
||||||
|
fmt.Fprintln(os.Stderr, "квесты: "+w)
|
||||||
|
}
|
||||||
|
for _, q := range loaded {
|
||||||
|
WayfarerQuestDefs = append(WayfarerQuestDefs, q)
|
||||||
|
existingKeys[q.Key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
291
wayfarer_quest_loader_test.go
Normal file
291
wayfarer_quest_loader_test.go
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func intPtr(n int) *int { return &n }
|
||||||
|
|
||||||
|
func validYAMLQuest() wayfarerQuestYAML {
|
||||||
|
return wayfarerQuestYAML{
|
||||||
|
Key: "quest.test_valid",
|
||||||
|
Title: wfLocalizedText{RU: "Тестовый квест", EN: "Test Quest"},
|
||||||
|
Brief: wfLocalizedText{RU: "Краткое описание", EN: "Brief description"},
|
||||||
|
Steps: []wayfarerQuestStepYAML{
|
||||||
|
{
|
||||||
|
Text: wfLocalizedText{RU: "Ситуация на шаге 0", EN: "Situation at step 0"},
|
||||||
|
Choices: []wayfarerQuestChoiceYAML{
|
||||||
|
{Label: wfLocalizedText{RU: "Идти дальше", EN: "Go on"}, NextStep: intPtr(1)},
|
||||||
|
{Label: wfLocalizedText{RU: "Отступить", EN: "Retreat"}, Terminal: true, Success: false,
|
||||||
|
Result: wfLocalizedText{RU: "Провал", EN: "Failure"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: wfLocalizedText{RU: "Ситуация на шаге 1", EN: "Situation at step 1"},
|
||||||
|
Choices: []wayfarerQuestChoiceYAML{
|
||||||
|
{Label: wfLocalizedText{RU: "Завершить", EN: "Finish"}, Terminal: true, Success: true,
|
||||||
|
Result: wfLocalizedText{RU: "Успех", EN: "Success"},
|
||||||
|
CreditsDelta: 500, HullDelta: -5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertYAMLQuestValid(t *testing.T) {
|
||||||
|
def, texts, err := convertYAMLQuest(validYAMLQuest(), "test.yaml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка: %v", err)
|
||||||
|
}
|
||||||
|
if def.Key != "quest.test_valid" {
|
||||||
|
t.Errorf("неверный ключ квеста: %q", def.Key)
|
||||||
|
}
|
||||||
|
if len(def.Steps) != 2 {
|
||||||
|
t.Fatalf("ожидалось 2 шага, получено %d", len(def.Steps))
|
||||||
|
}
|
||||||
|
if def.Steps[0].Choices[0].NextStep != 1 {
|
||||||
|
t.Errorf("ожидался NextStep=1 у первого выбора")
|
||||||
|
}
|
||||||
|
if def.Steps[0].Choices[1].Terminal != true || def.Steps[0].Choices[1].Success != false {
|
||||||
|
t.Errorf("второй выбор должен быть терминальным провалом")
|
||||||
|
}
|
||||||
|
if def.Steps[1].Choices[0].CreditsDelta != 500 || def.Steps[1].Choices[0].HullDelta != -5 {
|
||||||
|
t.Errorf("не перенеслись числовые последствия выбора")
|
||||||
|
}
|
||||||
|
if def.Steps[0].Choices[0].CargoIdx != -1 {
|
||||||
|
t.Errorf("ожидался CargoIdx=-1 по умолчанию, получено %d", def.Steps[0].Choices[0].CargoIdx)
|
||||||
|
}
|
||||||
|
if len(texts) == 0 {
|
||||||
|
t.Errorf("ожидались тексты для регистрации переводов")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertYAMLQuestMissingKey(t *testing.T) {
|
||||||
|
y := validYAMLQuest()
|
||||||
|
y.Key = ""
|
||||||
|
if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil {
|
||||||
|
t.Errorf("ожидалась ошибка при отсутствующем key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertYAMLQuestNoSteps(t *testing.T) {
|
||||||
|
y := validYAMLQuest()
|
||||||
|
y.Steps = nil
|
||||||
|
if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil {
|
||||||
|
t.Errorf("ожидалась ошибка при отсутствии шагов")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertYAMLQuestNoChoicesInStep(t *testing.T) {
|
||||||
|
y := validYAMLQuest()
|
||||||
|
y.Steps[0].Choices = nil
|
||||||
|
if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil {
|
||||||
|
t.Errorf("ожидалась ошибка при отсутствии вариантов выбора на шаге")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertYAMLQuestMissingNextStepOnNonTerminal(t *testing.T) {
|
||||||
|
y := validYAMLQuest()
|
||||||
|
y.Steps[0].Choices[0].NextStep = nil
|
||||||
|
if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil {
|
||||||
|
t.Errorf("ожидалась ошибка при отсутствии next_step у нетерминального выбора")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertYAMLQuestNextStepOutOfRange(t *testing.T) {
|
||||||
|
y := validYAMLQuest()
|
||||||
|
y.Steps[0].Choices[0].NextStep = intPtr(99)
|
||||||
|
if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil {
|
||||||
|
t.Errorf("ожидалась ошибка при next_step вне диапазона")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertYAMLQuestExplicitCargoIdx(t *testing.T) {
|
||||||
|
y := validYAMLQuest()
|
||||||
|
y.Steps[1].Choices[0].CargoIdx = intPtr(2)
|
||||||
|
y.Steps[1].Choices[0].CargoDelta = -3
|
||||||
|
def, _, err := convertYAMLQuest(y, "test.yaml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка: %v", err)
|
||||||
|
}
|
||||||
|
if def.Steps[1].Choices[0].CargoIdx != 2 || def.Steps[1].Choices[0].CargoDelta != -3 {
|
||||||
|
t.Errorf("явно указанный cargo_idx/cargo_delta не сохранился")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeYAMLFile(t *testing.T, dir, name, content string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatalf("не удалось записать тестовый файл: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadWayfarerQuestsFromDirMissingDirIsHarmless(t *testing.T) {
|
||||||
|
loaded, warnings := LoadWayfarerQuestsFromDir(filepath.Join(t.TempDir(), "does-not-exist"), nil)
|
||||||
|
if loaded != nil || warnings != nil {
|
||||||
|
t.Errorf("отсутствующая директория должна давать (nil, nil), получено (%v, %v)", loaded, warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadWayfarerQuestsFromDirValidFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
writeYAMLFile(t, dir, "myquest.yaml", `
|
||||||
|
key: quest.loader_test_ok
|
||||||
|
title: {ru: "Загруженный квест", en: "Loaded Quest"}
|
||||||
|
brief: {ru: "Описание", en: "Brief"}
|
||||||
|
steps:
|
||||||
|
- text: {ru: "Шаг 0", en: "Step 0"}
|
||||||
|
choices:
|
||||||
|
- label: {ru: "Дальше", en: "Onward"}
|
||||||
|
next_step: 1
|
||||||
|
- text: {ru: "Шаг 1", en: "Step 1"}
|
||||||
|
choices:
|
||||||
|
- label: {ru: "Конец", en: "The end"}
|
||||||
|
terminal: true
|
||||||
|
success: true
|
||||||
|
result: {ru: "Победа", en: "Victory"}
|
||||||
|
`)
|
||||||
|
loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil)
|
||||||
|
if len(warnings) != 0 {
|
||||||
|
t.Errorf("не ожидалось предупреждений, получено: %v", warnings)
|
||||||
|
}
|
||||||
|
if len(loaded) != 1 {
|
||||||
|
t.Fatalf("ожидался ровно 1 загруженный квест, получено %d", len(loaded))
|
||||||
|
}
|
||||||
|
if loaded[0].Key != "quest.loader_test_ok" {
|
||||||
|
t.Errorf("неверный ключ загруженного квеста: %q", loaded[0].Key)
|
||||||
|
}
|
||||||
|
if T("quest.loader_test_ok.title") != "Загруженный квест" {
|
||||||
|
t.Errorf("перевод названия не зарегистрировался в общей карте переводов, T() вернул: %q", T("quest.loader_test_ok.title"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadWayfarerQuestsFromDirSkipsMalformedYAML(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
writeYAMLFile(t, dir, "broken.yaml", "key: [это не валидный YAML для этой структуры\n - вложенность сломана")
|
||||||
|
loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil)
|
||||||
|
if len(loaded) != 0 {
|
||||||
|
t.Errorf("сломанный файл не должен был загрузиться, получено %d квестов", len(loaded))
|
||||||
|
}
|
||||||
|
if len(warnings) != 1 {
|
||||||
|
t.Fatalf("ожидалось ровно 1 предупреждение, получено %d: %v", len(warnings), warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadWayfarerQuestsFromDirSkipsDuplicateKey(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
writeYAMLFile(t, dir, "dup.yaml", `
|
||||||
|
key: quest.supernova
|
||||||
|
title: {ru: "Дубликат", en: "Duplicate"}
|
||||||
|
brief: {ru: "x", en: "x"}
|
||||||
|
steps:
|
||||||
|
- text: {ru: "x", en: "x"}
|
||||||
|
choices:
|
||||||
|
- label: {ru: "x", en: "x"}
|
||||||
|
terminal: true
|
||||||
|
success: true
|
||||||
|
result: {ru: "x", en: "x"}
|
||||||
|
`)
|
||||||
|
existing := map[string]bool{"quest.supernova": true}
|
||||||
|
loaded, warnings := LoadWayfarerQuestsFromDir(dir, existing)
|
||||||
|
if len(loaded) != 0 {
|
||||||
|
t.Errorf("квест с уже занятым ключом не должен был загрузиться")
|
||||||
|
}
|
||||||
|
if len(warnings) != 1 {
|
||||||
|
t.Fatalf("ожидалось предупреждение о занятом ключе, получено: %v", warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadWayfarerQuestsFromDirSkipsIncompleteTranslation(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
writeYAMLFile(t, dir, "incomplete.yaml", `
|
||||||
|
key: quest.loader_test_incomplete
|
||||||
|
title: {ru: "Есть по-русски", en: ""}
|
||||||
|
brief: {ru: "x", en: "x"}
|
||||||
|
steps:
|
||||||
|
- text: {ru: "x", en: "x"}
|
||||||
|
choices:
|
||||||
|
- label: {ru: "x", en: "x"}
|
||||||
|
terminal: true
|
||||||
|
success: true
|
||||||
|
result: {ru: "x", en: "x"}
|
||||||
|
`)
|
||||||
|
loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil)
|
||||||
|
if len(loaded) != 0 {
|
||||||
|
t.Errorf("квест с неполным переводом не должен был загрузиться")
|
||||||
|
}
|
||||||
|
if len(warnings) == 0 {
|
||||||
|
t.Errorf("ожидалось предупреждение о неполном переводе")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadWayfarerQuestsFromDirIgnoresNonYAMLFiles(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
writeYAMLFile(t, dir, "readme.txt", "это не квест, просто заметка")
|
||||||
|
loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil)
|
||||||
|
if len(loaded) != 0 || len(warnings) != 0 {
|
||||||
|
t.Errorf("файлы не .yaml/.yml должны молча игнорироваться, получено loaded=%v warnings=%v", loaded, warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoadedQuestIsFullyPlayable проверяет, что загруженный из YAML
|
||||||
|
// квест не просто структурно корректен, а реально проходим через
|
||||||
|
// обычный игровой цикл (StartQuest -> ChooseQuestOption -> ...).
|
||||||
|
func TestLoadedQuestIsFullyPlayable(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
writeYAMLFile(t, dir, "playable.yaml", `
|
||||||
|
key: quest.loader_test_playable
|
||||||
|
title: {ru: "Играбельный квест", en: "Playable Quest"}
|
||||||
|
brief: {ru: "x", en: "x"}
|
||||||
|
steps:
|
||||||
|
- text: {ru: "Шаг 0", en: "Step 0"}
|
||||||
|
choices:
|
||||||
|
- label: {ru: "Вариант A", en: "Option A"}
|
||||||
|
next_step: 1
|
||||||
|
- text: {ru: "Шаг 1", en: "Step 1"}
|
||||||
|
choices:
|
||||||
|
- label: {ru: "Завершить успешно", en: "Finish successfully"}
|
||||||
|
terminal: true
|
||||||
|
success: true
|
||||||
|
result: {ru: "Готово", en: "Done"}
|
||||||
|
credits_delta: 250
|
||||||
|
`)
|
||||||
|
loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil)
|
||||||
|
if len(warnings) != 0 {
|
||||||
|
t.Fatalf("не ожидалось предупреждений: %v", warnings)
|
||||||
|
}
|
||||||
|
if len(loaded) != 1 {
|
||||||
|
t.Fatalf("ожидался 1 квест, получено %d", len(loaded))
|
||||||
|
}
|
||||||
|
|
||||||
|
oldDefs := WayfarerQuestDefs
|
||||||
|
WayfarerQuestDefs = append(append([]WayfarerQuestDef{}, oldDefs...), loaded...)
|
||||||
|
defer func() { WayfarerQuestDefs = oldDefs }()
|
||||||
|
|
||||||
|
s := NewWayfarerShip(WayfarerSystemID{})
|
||||||
|
creditsBefore := s.Credits
|
||||||
|
if err := s.StartQuest("quest.loader_test_playable"); err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка запуска квеста: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := s.ChooseQuestOption(0); err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка выбора на шаге 0: %v", err)
|
||||||
|
}
|
||||||
|
step, ok := s.CurrentQuestStep()
|
||||||
|
if !ok || T(step.TextKey) != "Шаг 1" {
|
||||||
|
t.Fatalf("ожидался переход на шаг 1, получено: %+v (ok=%v)", step, ok)
|
||||||
|
}
|
||||||
|
if _, err := s.ChooseQuestOption(0); err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка завершающего выбора: %v", err)
|
||||||
|
}
|
||||||
|
if s.ActiveQuest != nil {
|
||||||
|
t.Errorf("квест должен был завершиться")
|
||||||
|
}
|
||||||
|
if !s.CompletedQuests["quest.loader_test_playable"] {
|
||||||
|
t.Errorf("квест должен был отметиться как завершённый успешно")
|
||||||
|
}
|
||||||
|
if s.Credits != creditsBefore+250 {
|
||||||
|
t.Errorf("награда за квест не начислилась: было %v, стало %v", creditsBefore, s.Credits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -73,6 +73,10 @@ type WayfarerModel struct {
|
||||||
|
|
||||||
pendingQuestKey string // ключ квеста, предложенного встречей в пути, но ещё не принятого/отклонённого
|
pendingQuestKey string // ключ квеста, предложенного встречей в пути, но ещё не принятого/отклонённого
|
||||||
|
|
||||||
|
marketPrices []float64 // цены на товары в текущей системе — считаются один раз при стыковке, не на каждый рендер (см. rollMarketPrices)
|
||||||
|
|
||||||
|
confirmingReset bool // ждём подтверждения "начать новую карьеру" (y/n) — необратимое действие
|
||||||
|
|
||||||
message string
|
message string
|
||||||
isError bool
|
isError bool
|
||||||
|
|
||||||
|
|
@ -88,10 +92,14 @@ type WayfarerModel struct {
|
||||||
// начинает новую с фиксированной стартовой системы.
|
// начинает новую с фиксированной стартовой системы.
|
||||||
func NewWayfarerModel() WayfarerModel {
|
func NewWayfarerModel() WayfarerModel {
|
||||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
var m WayfarerModel
|
||||||
if ship, err := LoadWayfarerShip(); err == nil {
|
if ship, err := LoadWayfarerShip(); err == nil {
|
||||||
return WayfarerModel{ship: ship, view: wfViewHub, rng: rng, termWidth: 80}
|
m = WayfarerModel{ship: ship, view: wfViewHub, rng: rng, termWidth: 80}
|
||||||
|
} else {
|
||||||
|
m = WayfarerModel{ship: NewWayfarerShip(wayfarerStartSystem), view: wfViewHub, rng: rng, termWidth: 80}
|
||||||
}
|
}
|
||||||
return WayfarerModel{ship: NewWayfarerShip(wayfarerStartSystem), view: wfViewHub, rng: rng, termWidth: 80}
|
m.rollMarketPrices()
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m WayfarerModel) Init() tea.Cmd { return nil }
|
func (m WayfarerModel) Init() tea.Cmd { return nil }
|
||||||
|
|
@ -115,6 +123,19 @@ func (m WayfarerModel) currentSystem() WayfarerSystem {
|
||||||
return GenerateSystem(m.ship.CurrentSystem)
|
return GenerateSystem(m.ship.CurrentSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rollMarketPrices пересчитывает цены на товары в текущей системе.
|
||||||
|
// Вызывается один раз при стыковке (старт игры, загрузка карьеры,
|
||||||
|
// успешный прыжок) — а не при каждом рендере рынка, иначе цены
|
||||||
|
// мелькали бы на каждое нажатие клавиши, даже просто при движении
|
||||||
|
// курсора по списку товаров.
|
||||||
|
func (m *WayfarerModel) rollMarketPrices() {
|
||||||
|
sys := m.currentSystem()
|
||||||
|
m.marketPrices = make([]float64, len(WayfarerCommodities))
|
||||||
|
for i := range WayfarerCommodities {
|
||||||
|
m.marketPrices[i] = CommodityPrice(i, sys, m.rng)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m WayfarerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m WayfarerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
|
if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
|
||||||
m.termWidth = sizeMsg.Width
|
m.termWidth = sizeMsg.Width
|
||||||
|
|
@ -135,6 +156,24 @@ func (m WayfarerModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.confirmingReset {
|
||||||
|
switch key {
|
||||||
|
case "y", "Y":
|
||||||
|
_ = DeleteWayfarerSave()
|
||||||
|
m.ship = NewWayfarerShip(wayfarerStartSystem)
|
||||||
|
m.confirmingReset = false
|
||||||
|
m.combat = nil
|
||||||
|
m.pendingQuestKey = ""
|
||||||
|
m.view = wfViewHub
|
||||||
|
m.rollMarketPrices()
|
||||||
|
m.setInfo("wf.msg.reset_done")
|
||||||
|
case "n", "N", "esc":
|
||||||
|
m.confirmingReset = false
|
||||||
|
m.message = ""
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
switch m.view {
|
switch m.view {
|
||||||
case wfViewHub:
|
case wfViewHub:
|
||||||
return m.handleHubKey(key)
|
return m.handleHubKey(key)
|
||||||
|
|
@ -183,6 +222,8 @@ func (m WayfarerModel) handleHubKey(key string) (tea.Model, tea.Cmd) {
|
||||||
m.view = wfViewQuestList
|
m.view = wfViewQuestList
|
||||||
m.cursor = 0
|
m.cursor = 0
|
||||||
m.message = ""
|
m.message = ""
|
||||||
|
case "N":
|
||||||
|
m.confirmingReset = true
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
@ -204,8 +245,7 @@ func (m WayfarerModel) handleMarketKey(key string) (tea.Model, tea.Cmd) {
|
||||||
m.cursor++
|
m.cursor++
|
||||||
}
|
}
|
||||||
case "b", "s", "B", "S":
|
case "b", "s", "B", "S":
|
||||||
sys := m.currentSystem()
|
price := m.marketPrices[m.cursor]
|
||||||
price := CommodityPrice(m.cursor, sys, m.rng)
|
|
||||||
switch key {
|
switch key {
|
||||||
case "b":
|
case "b":
|
||||||
if err := m.ship.BuyCommodity(m.cursor, 1, price); err != nil {
|
if err := m.ship.BuyCommodity(m.cursor, 1, price); err != nil {
|
||||||
|
|
@ -395,6 +435,7 @@ func (m WayfarerModel) performGalacticJump() (tea.Model, tea.Cmd) {
|
||||||
m.ship.Fuel = 0
|
m.ship.Fuel = 0
|
||||||
m.ship.CurrentSystem = target
|
m.ship.CurrentSystem = target
|
||||||
m.ship.Visited[target] = true
|
m.ship.Visited[target] = true
|
||||||
|
m.rollMarketPrices()
|
||||||
m.setInfo("wf.msg.galactic_jump")
|
m.setInfo("wf.msg.galactic_jump")
|
||||||
m.view = wfViewHub
|
m.view = wfViewHub
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -409,6 +450,7 @@ func (m WayfarerModel) performJump(target WayfarerSystemID) (tea.Model, tea.Cmd)
|
||||||
m.setError(err)
|
m.setError(err)
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
m.rollMarketPrices()
|
||||||
|
|
||||||
dest := GenerateSystem(target)
|
dest := GenerateSystem(target)
|
||||||
encounter := RollEncounter(dest, m.rng)
|
encounter := RollEncounter(dest, m.rng)
|
||||||
|
|
@ -649,6 +691,15 @@ func (m WayfarerModel) View() string {
|
||||||
if m.quitting {
|
if m.quitting {
|
||||||
return T("common.goodbye")
|
return T("common.goodbye")
|
||||||
}
|
}
|
||||||
|
if m.confirmingReset {
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintln(&b, titleStyle.Render(T("wf.title")))
|
||||||
|
fmt.Fprintln(&b)
|
||||||
|
fmt.Fprintln(&b, errorStyle.Render(T("wf.confirm_reset")))
|
||||||
|
fmt.Fprintln(&b)
|
||||||
|
fmt.Fprintln(&b, dimStyle.Render(T("wf.confirm_reset.hint")))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
switch m.view {
|
switch m.view {
|
||||||
case wfViewMarket:
|
case wfViewMarket:
|
||||||
return m.viewMarket()
|
return m.viewMarket()
|
||||||
|
|
@ -725,14 +776,13 @@ func (m WayfarerModel) viewHub() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m WayfarerModel) viewMarket() string {
|
func (m WayfarerModel) viewMarket() string {
|
||||||
sys := m.currentSystem()
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
fmt.Fprintln(&b, titleStyle.Render(T("wf.market.title")))
|
fmt.Fprintln(&b, titleStyle.Render(T("wf.market.title")))
|
||||||
fmt.Fprintln(&b)
|
fmt.Fprintln(&b)
|
||||||
fmt.Fprintln(&b, m.renderShipStatus())
|
fmt.Fprintln(&b, m.renderShipStatus())
|
||||||
fmt.Fprintln(&b)
|
fmt.Fprintln(&b)
|
||||||
for i, c := range WayfarerCommodities {
|
for i, c := range WayfarerCommodities {
|
||||||
price := CommodityPrice(i, sys, m.rng)
|
price := m.marketPrices[i]
|
||||||
line := Tf("wf.market.line", T(c.Key), price, m.ship.Cargo[i])
|
line := Tf("wf.market.line", T(c.Key), price, m.ship.Cargo[i])
|
||||||
if m.ship.Cargo[i] > 0 {
|
if m.ship.Cargo[i] > 0 {
|
||||||
line += Tf("wf.market.bought_at", m.ship.CargoCostBasis[i])
|
line += Tf("wf.market.bought_at", m.ship.CargoCostBasis[i])
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,137 @@ func TestWayfarerQuestOfferDecline(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWayfarerMarketPricesStableUntilNextDocking(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewWayfarerModel()
|
||||||
|
m.view = wfViewMarket
|
||||||
|
|
||||||
|
before := append([]float64{}, m.marketPrices...)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
next, _ := m.Update(key("down"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
next, _ = m.Update(key("up"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
}
|
||||||
|
for i, p := range m.marketPrices {
|
||||||
|
if p != before[i] {
|
||||||
|
t.Errorf("цена товара %d изменилась просто от навигации по рынку: было %v, стало %v", i, before[i], p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWayfarerMarketPricesChangeAfterJump(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewWayfarerModel()
|
||||||
|
before := append([]float64{}, m.marketPrices...)
|
||||||
|
|
||||||
|
nearby := wayfarerNearbySystems(m.ship.CurrentSystem, m.ship.FuelRange())
|
||||||
|
if len(nearby) == 0 {
|
||||||
|
t.Fatal("нет достижимых систем для проверки")
|
||||||
|
}
|
||||||
|
next, _ := m.performJump(nearby[0])
|
||||||
|
m2 := next.(WayfarerModel)
|
||||||
|
|
||||||
|
changed := false
|
||||||
|
for i, p := range m2.marketPrices {
|
||||||
|
if p != before[i] {
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
t.Errorf("после прыжка в новую систему цены должны были пересчитаться")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWayfarerBuySellUsesCachedPriceNotFreshRoll(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewWayfarerModel()
|
||||||
|
m.view = wfViewMarket
|
||||||
|
m.cursor = 0
|
||||||
|
cachedPrice := m.marketPrices[0]
|
||||||
|
|
||||||
|
next, _ := m.Update(key("b"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
// после покупки кэш цены не должен был поменяться сам по себе
|
||||||
|
if m.marketPrices[0] != cachedPrice {
|
||||||
|
t.Errorf("покупка не должна была пересчитать цену: было %v, стало %v", cachedPrice, m.marketPrices[0])
|
||||||
|
}
|
||||||
|
wantCredits := 1000 - cachedPrice
|
||||||
|
if m.ship.Credits != wantCredits {
|
||||||
|
t.Errorf("покупка должна была списать ровно закэшированную цену %v, кредитов осталось %v (ожидалось %v)",
|
||||||
|
cachedPrice, m.ship.Credits, wantCredits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWayfarerResetRequestShowsConfirmation(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewWayfarerModel()
|
||||||
|
next, _ := m.Update(key("N"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
if !m.confirmingReset {
|
||||||
|
t.Fatalf("ожидался запрос подтверждения после нажатия N")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWayfarerResetCancelKeepsCareer(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewWayfarerModel()
|
||||||
|
m.ship.Credits = 5000
|
||||||
|
|
||||||
|
next, _ := m.Update(key("N"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
next, _ = m.Update(key("n"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
|
||||||
|
if m.confirmingReset {
|
||||||
|
t.Errorf("запрос подтверждения должен был закрыться после отмены")
|
||||||
|
}
|
||||||
|
if m.ship.Credits != 5000 {
|
||||||
|
t.Errorf("карьера не должна была сброситься при отмене, получено кредитов: %v", m.ship.Credits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWayfarerResetConfirmStartsFreshCareer(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewWayfarerModel()
|
||||||
|
m.ship.Credits = 5000
|
||||||
|
m.ship.CurrentSystem = WayfarerSystemID{Galaxy: 2, Index: 50}
|
||||||
|
|
||||||
|
next, _ := m.Update(key("N"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
next, _ = m.Update(key("y"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
|
||||||
|
if m.confirmingReset {
|
||||||
|
t.Errorf("запрос подтверждения должен был закрыться после подтверждения")
|
||||||
|
}
|
||||||
|
if m.ship.Credits != 1000 {
|
||||||
|
t.Errorf("ожидались стартовые 1000 кредитов после новой карьеры, получено %v", m.ship.Credits)
|
||||||
|
}
|
||||||
|
if m.ship.CurrentSystem != wayfarerStartSystem {
|
||||||
|
t.Errorf("новая карьера должна была начаться со стартовой системы")
|
||||||
|
}
|
||||||
|
if m.view != wfViewHub {
|
||||||
|
t.Errorf("после сброса должен был открыться хаб")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWayfarerResetActionsIgnoredWhileConfirming(t *testing.T) {
|
||||||
|
withTempConfigDir(t)
|
||||||
|
m := NewWayfarerModel()
|
||||||
|
viewBefore := m.view
|
||||||
|
|
||||||
|
next, _ := m.Update(key("N"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
// нажатие "1" (переход на рынок) не должно срабатывать, пока ждём подтверждения
|
||||||
|
next, _ = m.Update(key("1"))
|
||||||
|
m = next.(WayfarerModel)
|
||||||
|
if m.view != viewBefore {
|
||||||
|
t.Errorf("обычная навигация не должна срабатывать во время запроса подтверждения")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWfWrapClampsExcessiveWidth(t *testing.T) {
|
func TestWfWrapClampsExcessiveWidth(t *testing.T) {
|
||||||
text := "текст"
|
text := "текст"
|
||||||
wrapped := wfWrap(text, 500)
|
wrapped := wfWrap(text, 500)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue