mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-21 12:16:54 +02:00
270 lines
6.7 KiB
Go
270 lines
6.7 KiB
Go
package jsonschema
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/iancoleman/orderedmap"
|
|
"github.com/jesseduffield/lazycore/pkg/utils"
|
|
"github.com/samber/lo"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Node struct {
|
|
Name string
|
|
Description string
|
|
Default any
|
|
Children []*Node
|
|
}
|
|
|
|
const (
|
|
IndentLevel = 2
|
|
DocumentationCommentStart = "<!-- START CONFIG YAML: AUTOMATICALLY GENERATED with `go generate ./..., DO NOT UPDATE MANUALLY -->\n"
|
|
DocumentationCommentEnd = "<!-- END CONFIG YAML -->"
|
|
DocumentationCommentStartLen = len(DocumentationCommentStart)
|
|
)
|
|
|
|
func insertBlankLines(buffer bytes.Buffer) bytes.Buffer {
|
|
lines := strings.Split(strings.TrimRight(buffer.String(), "\n"), "\n")
|
|
|
|
var newBuffer bytes.Buffer
|
|
|
|
previousIndent := -1
|
|
wasComment := false
|
|
|
|
for _, line := range lines {
|
|
trimmedLine := strings.TrimLeft(line, " ")
|
|
indent := len(line) - len(trimmedLine)
|
|
isComment := strings.HasPrefix(trimmedLine, "#")
|
|
if isComment && !wasComment && indent <= previousIndent {
|
|
newBuffer.WriteString("\n")
|
|
}
|
|
newBuffer.WriteString(line)
|
|
newBuffer.WriteString("\n")
|
|
previousIndent = indent
|
|
wasComment = isComment
|
|
}
|
|
|
|
return newBuffer
|
|
}
|
|
|
|
func prepareMarshalledConfig(buffer bytes.Buffer) []byte {
|
|
buffer = insertBlankLines(buffer)
|
|
|
|
// Remove all `---` lines
|
|
lines := strings.Split(strings.TrimRight(buffer.String(), "\n"), "\n")
|
|
|
|
var newBuffer bytes.Buffer
|
|
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) != "---" {
|
|
newBuffer.WriteString(line)
|
|
newBuffer.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
config := newBuffer.Bytes()
|
|
|
|
// Add markdown yaml block tag
|
|
config = append([]byte("```yaml\n"), config...)
|
|
config = append(config, []byte("```\n")...)
|
|
|
|
return config
|
|
}
|
|
|
|
func setComment(yamlNode *yaml.Node, description string) {
|
|
// Workaround for the way yaml formats the HeadComment if it contains
|
|
// blank lines: it renders these without a leading "#", but we want a
|
|
// leading "#" even on blank lines. However, yaml respects it if the
|
|
// HeadComment already contains a leading "#", so we prefix all lines
|
|
// (including blank ones) with "#".
|
|
yamlNode.HeadComment = strings.Join(
|
|
lo.Map(strings.Split(description, "\n"), func(s string, _ int) string {
|
|
if s == "" {
|
|
return "#" // avoid trailing space on blank lines
|
|
}
|
|
return "# " + s
|
|
}),
|
|
"\n")
|
|
}
|
|
|
|
func (n *Node) MarshalYAML() (interface{}, error) {
|
|
node := yaml.Node{
|
|
Kind: yaml.MappingNode,
|
|
}
|
|
|
|
keyNode := yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
Value: n.Name,
|
|
}
|
|
if n.Description != "" {
|
|
setComment(&keyNode, n.Description)
|
|
}
|
|
|
|
if n.Default != nil {
|
|
valueNode := yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
}
|
|
err := valueNode.Encode(n.Default)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
node.Content = append(node.Content, &keyNode, &valueNode)
|
|
} else if len(n.Children) > 0 {
|
|
childrenNode := yaml.Node{
|
|
Kind: yaml.MappingNode,
|
|
}
|
|
for _, child := range n.Children {
|
|
childYaml, err := child.MarshalYAML()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
childKey := yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
Value: child.Name,
|
|
}
|
|
if child.Description != "" {
|
|
setComment(&childKey, child.Description)
|
|
}
|
|
childYaml = childYaml.(*yaml.Node)
|
|
childrenNode.Content = append(childrenNode.Content, childYaml.(*yaml.Node).Content...)
|
|
}
|
|
node.Content = append(node.Content, &keyNode, &childrenNode)
|
|
}
|
|
|
|
return &node, nil
|
|
}
|
|
|
|
func getDescription(v *orderedmap.OrderedMap) string {
|
|
description, ok := v.Get("description")
|
|
if !ok {
|
|
description = ""
|
|
}
|
|
return description.(string)
|
|
}
|
|
|
|
func getDefault(v *orderedmap.OrderedMap) (error, any) {
|
|
defaultValue, ok := v.Get("default")
|
|
if ok {
|
|
return nil, defaultValue
|
|
}
|
|
|
|
dataType, ok := v.Get("type")
|
|
if ok {
|
|
dataTypeString := dataType.(string)
|
|
if dataTypeString == "string" {
|
|
return nil, ""
|
|
}
|
|
}
|
|
|
|
return errors.New("Failed to get default value"), nil
|
|
}
|
|
|
|
func parseNode(parent *Node, name string, value *orderedmap.OrderedMap) {
|
|
description := getDescription(value)
|
|
err, defaultValue := getDefault(value)
|
|
if err == nil {
|
|
leaf := &Node{Name: name, Description: description, Default: defaultValue}
|
|
parent.Children = append(parent.Children, leaf)
|
|
}
|
|
|
|
properties, ok := value.Get("properties")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
orderedProperties := properties.(orderedmap.OrderedMap)
|
|
|
|
node := &Node{Name: name, Description: description}
|
|
parent.Children = append(parent.Children, node)
|
|
|
|
keys := orderedProperties.Keys()
|
|
for _, name := range keys {
|
|
value, _ := orderedProperties.Get(name)
|
|
typedValue := value.(orderedmap.OrderedMap)
|
|
parseNode(node, name, &typedValue)
|
|
}
|
|
}
|
|
|
|
func writeToConfigDocs(config []byte) error {
|
|
configPath := utils.GetLazyRootDirectory() + "/docs/Config.md"
|
|
markdown, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("Error reading Config.md file %w", err)
|
|
}
|
|
|
|
startConfigSectionIndex := bytes.Index(markdown, []byte(DocumentationCommentStart))
|
|
if startConfigSectionIndex == -1 {
|
|
return errors.New("Default config starting comment not found")
|
|
}
|
|
|
|
endConfigSectionIndex := bytes.Index(markdown[startConfigSectionIndex+DocumentationCommentStartLen:], []byte(DocumentationCommentEnd))
|
|
if endConfigSectionIndex == -1 {
|
|
return errors.New("Default config closing comment not found")
|
|
}
|
|
|
|
endConfigSectionIndex = endConfigSectionIndex + startConfigSectionIndex + DocumentationCommentStartLen
|
|
|
|
newMarkdown := make([]byte, 0, len(markdown)-endConfigSectionIndex+startConfigSectionIndex+len(config))
|
|
newMarkdown = append(newMarkdown, markdown[:startConfigSectionIndex+DocumentationCommentStartLen]...)
|
|
newMarkdown = append(newMarkdown, config...)
|
|
newMarkdown = append(newMarkdown, markdown[endConfigSectionIndex:]...)
|
|
|
|
if err := os.WriteFile(configPath, newMarkdown, 0o644); err != nil {
|
|
return fmt.Errorf("Error writing to file %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GenerateConfigDocs() {
|
|
content, err := os.ReadFile(GetSchemaDir() + "/config.json")
|
|
if err != nil {
|
|
panic("Error reading config.json")
|
|
}
|
|
|
|
schema := orderedmap.New()
|
|
|
|
err = json.Unmarshal(content, &schema)
|
|
if err != nil {
|
|
panic("Failed to unmarshal config.json")
|
|
}
|
|
|
|
root, ok := schema.Get("properties")
|
|
if !ok {
|
|
panic("properties key not found in schema")
|
|
}
|
|
orderedRoot := root.(orderedmap.OrderedMap)
|
|
|
|
rootNode := Node{}
|
|
for _, name := range orderedRoot.Keys() {
|
|
value, _ := orderedRoot.Get(name)
|
|
typedValue := value.(orderedmap.OrderedMap)
|
|
parseNode(&rootNode, name, &typedValue)
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
encoder := yaml.NewEncoder(&buffer)
|
|
encoder.SetIndent(IndentLevel)
|
|
|
|
for _, child := range rootNode.Children {
|
|
err := encoder.Encode(child)
|
|
if err != nil {
|
|
panic("Failed to Marshal document")
|
|
}
|
|
}
|
|
encoder.Close()
|
|
|
|
config := prepareMarshalledConfig(buffer)
|
|
|
|
err = writeToConfigDocs(config)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|