mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-08 06:45:38 +00:00
195 lines
5.9 KiB
Go
195 lines
5.9 KiB
Go
package yqlib
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DateTimePreprocessor handles automatic timestamp tagging for Goccy parser compatibility
|
|
type DateTimePreprocessor struct {
|
|
enabled bool
|
|
}
|
|
|
|
// NewDateTimePreprocessor creates a new datetime preprocessor
|
|
func NewDateTimePreprocessor(enabled bool) *DateTimePreprocessor {
|
|
return &DateTimePreprocessor{enabled: enabled}
|
|
}
|
|
|
|
// isValidDateTime checks if a string represents a valid ISO8601/RFC3339 datetime
|
|
func isValidDateTime(value string) bool {
|
|
// Try to parse with RFC3339 first
|
|
if _, err := time.Parse(time.RFC3339, value); err == nil {
|
|
return true
|
|
}
|
|
|
|
// Try to parse date-only format
|
|
if _, err := time.Parse(time.DateOnly, value); err == nil {
|
|
return true
|
|
}
|
|
|
|
// Try RFC3339 without timezone
|
|
if _, err := time.Parse(strings.TrimSuffix(time.RFC3339, "Z07:00"), value); err == nil {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// PreprocessDocument automatically adds !!timestamp tags to ISO8601 datetime strings
|
|
// in YAML documents when using Goccy parser for consistent datetime arithmetic behaviour
|
|
func (dtp *DateTimePreprocessor) PreprocessDocument(yamlContent string) string {
|
|
if !dtp.enabled {
|
|
return yamlContent
|
|
}
|
|
|
|
lines := strings.Split(yamlContent, "\n")
|
|
var result strings.Builder
|
|
|
|
for i, line := range lines {
|
|
processed := dtp.processLine(line)
|
|
result.WriteString(processed)
|
|
|
|
// Add newline except for the last line (preserve original ending)
|
|
if i < len(lines)-1 {
|
|
result.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
return result.String()
|
|
}
|
|
|
|
// processLine processes a single line of YAML, adding timestamp tags where appropriate
|
|
func (dtp *DateTimePreprocessor) processLine(line string) string {
|
|
// Skip lines that are comments, already have tags, or are part of multi-line constructs
|
|
if isSkippableLine(line) {
|
|
return line
|
|
}
|
|
|
|
// Look for key-value pairs: "key: value" or "- value"
|
|
trimLeft := strings.TrimLeft(line, " \t")
|
|
if strings.HasPrefix(trimLeft, "- ") {
|
|
// Handle array items first (before key-value pairs)
|
|
return dtp.processArrayItemLine(line)
|
|
} else if colonIndex := strings.Index(line, ":"); colonIndex != -1 {
|
|
// Handle map key-value pairs
|
|
return dtp.processKeyValueLine(line, colonIndex)
|
|
}
|
|
|
|
return line
|
|
}
|
|
|
|
// isSkippableLine checks if a line should be skipped from datetime preprocessing
|
|
func isSkippableLine(line string) bool {
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
// Skip empty lines, comments, directives, document separators
|
|
switch {
|
|
case trimmed == "" || strings.HasPrefix(trimmed, "#") ||
|
|
strings.HasPrefix(trimmed, "%") || strings.HasPrefix(trimmed, "---") ||
|
|
strings.HasPrefix(trimmed, "..."):
|
|
return true
|
|
}
|
|
|
|
// Skip lines that already have explicit tags
|
|
if strings.Contains(line, "!!") {
|
|
return true
|
|
}
|
|
|
|
// Skip multi-line scalar indicators
|
|
switch {
|
|
case strings.HasSuffix(trimmed, "|") || strings.HasSuffix(trimmed, ">") ||
|
|
strings.HasSuffix(trimmed, "|-") || strings.HasSuffix(trimmed, ">-") ||
|
|
strings.HasSuffix(trimmed, "|+") || strings.HasSuffix(trimmed, ">+"):
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// processKeyValueLine processes a line containing a key-value pair
|
|
func (dtp *DateTimePreprocessor) processKeyValueLine(line string, colonIndex int) string {
|
|
beforeColon := line[:colonIndex]
|
|
afterColon := line[colonIndex+1:]
|
|
|
|
// Check if the value part looks like a datetime
|
|
trimmedValue := strings.TrimSpace(afterColon)
|
|
|
|
// Skip if value is empty, quoted, or already complex
|
|
if trimmedValue == "" ||
|
|
strings.HasPrefix(trimmedValue, `"`) ||
|
|
strings.HasPrefix(trimmedValue, "'") ||
|
|
strings.HasPrefix(trimmedValue, "{") ||
|
|
strings.HasPrefix(trimmedValue, "[") ||
|
|
strings.HasPrefix(trimmedValue, "&") ||
|
|
strings.HasPrefix(trimmedValue, "*") {
|
|
return line
|
|
}
|
|
|
|
// Check if it matches datetime patterns and is valid
|
|
if dtp.matchesDateTimePattern(trimmedValue) && isValidDateTime(trimmedValue) {
|
|
// Insert !!timestamp tag before the value
|
|
return beforeColon + ": !!timestamp " + trimmedValue
|
|
}
|
|
|
|
return line
|
|
}
|
|
|
|
// processArrayItemLine processes a line containing an array item
|
|
func (dtp *DateTimePreprocessor) processArrayItemLine(line string) string {
|
|
// Find the position of "- " after any leading whitespace
|
|
trimLeft := strings.TrimLeft(line, " \t")
|
|
if !strings.HasPrefix(trimLeft, "- ") {
|
|
return line
|
|
}
|
|
|
|
// Find the actual position of "- " in the original line
|
|
leadingWhitespace := line[:len(line)-len(trimLeft)]
|
|
dashIndex := len(leadingWhitespace)
|
|
|
|
beforeDash := line[:dashIndex+2] // Include leading whitespace and "- "
|
|
afterDash := line[dashIndex+2:]
|
|
|
|
trimmedValue := strings.TrimSpace(afterDash)
|
|
|
|
// Skip if value is empty, quoted, or already complex
|
|
switch {
|
|
case trimmedValue == "" ||
|
|
strings.HasPrefix(trimmedValue, "\"") ||
|
|
strings.HasPrefix(trimmedValue, "'") ||
|
|
strings.HasPrefix(trimmedValue, "{") ||
|
|
strings.HasPrefix(trimmedValue, "[") ||
|
|
strings.HasPrefix(trimmedValue, "&") ||
|
|
strings.HasPrefix(trimmedValue, "*"):
|
|
return line
|
|
}
|
|
|
|
// Check if it matches datetime patterns and is valid
|
|
if dtp.matchesDateTimePattern(trimmedValue) && isValidDateTime(trimmedValue) {
|
|
// Insert !!timestamp tag before the value
|
|
return beforeDash + "!!timestamp " + trimmedValue
|
|
}
|
|
|
|
return line
|
|
}
|
|
|
|
// ISO8601 date/time patterns that should be automatically tagged as timestamps
|
|
var dateTimePatterns = []*regexp.Regexp{
|
|
// RFC3339 / ISO8601 with timezone: 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00
|
|
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2}))\s*$`),
|
|
// Date only: 2006-01-02
|
|
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2})\s*$`),
|
|
// RFC3339 without timezone: 2006-01-02T15:04:05
|
|
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?)\s*$`),
|
|
}
|
|
|
|
// matchesDateTimePattern checks if a value matches any of the datetime regex patterns
|
|
func (dtp *DateTimePreprocessor) matchesDateTimePattern(value string) bool {
|
|
for _, pattern := range dateTimePatterns {
|
|
if pattern.MatchString(value) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|