Implement artifact-level render command with Markdown output and update docs
This commit is contained in:
75
internal/render/markdown.go
Normal file
75
internal/render/markdown.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MarkdownRenderer renders transcript artifacts as Markdown.
|
||||
type MarkdownRenderer struct{}
|
||||
|
||||
// Render renders the transcript into deterministic Markdown.
|
||||
func (MarkdownRenderer) Render(transcript Transcript, opts Options) (string, error) {
|
||||
var lines []string
|
||||
|
||||
title := strings.TrimSpace(opts.Title)
|
||||
if title == "" {
|
||||
title = "Transcript"
|
||||
}
|
||||
lines = append(lines, "# "+title, "")
|
||||
|
||||
if opts.IncludeMetadata {
|
||||
lines = append(lines,
|
||||
fmt.Sprintf("- Application: %s", transcript.Metadata.Application),
|
||||
fmt.Sprintf("- Version: %s", transcript.Metadata.Version),
|
||||
fmt.Sprintf("- Output schema: %s", transcript.Schema),
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
for _, segment := range transcript.Segments {
|
||||
parts := make([]string, 0, 4)
|
||||
if opts.IncludeTimestamps {
|
||||
parts = append(parts, fmt.Sprintf("[%s–%s]", formatTimestamp(segment.Start), formatTimestamp(segment.End)))
|
||||
}
|
||||
if opts.IncludeSegmentIDs {
|
||||
parts = append(parts, fmt.Sprintf("[#%d]", segment.ID))
|
||||
}
|
||||
|
||||
text := segment.Text
|
||||
if shouldItalicize(segment.Categories) {
|
||||
text = "*" + text + "*"
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("**%s:** %s", segment.Speaker, text))
|
||||
lines = append(lines, strings.Join(parts, " "))
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
output := strings.Join(lines, "\n")
|
||||
if !strings.HasSuffix(output, "\n") {
|
||||
output += "\n"
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func shouldItalicize(categories []string) bool {
|
||||
for _, category := range categories {
|
||||
switch category {
|
||||
case "background", "backchannel", "filler":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatTimestamp(seconds float64) string {
|
||||
total := int(math.Round(seconds))
|
||||
if total < 0 {
|
||||
total = 0
|
||||
}
|
||||
hours := total / 3600
|
||||
minutes := (total % 3600) / 60
|
||||
remainder := total % 60
|
||||
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, remainder)
|
||||
}
|
||||
140
internal/render/markdown_test.go
Normal file
140
internal/render/markdown_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarkdownRendererDefaultTranscriptShape(t *testing.T) {
|
||||
transcript := Transcript{
|
||||
Schema: "seriatim-intermediate",
|
||||
Metadata: Metadata{
|
||||
Application: "seriatim",
|
||||
Version: "v-test",
|
||||
},
|
||||
Segments: []Segment{
|
||||
{ID: 1, Start: 1, End: 4, Speaker: "Eric", Text: "Hello there."},
|
||||
{ID: 2, Start: 5, End: 8, Speaker: "Mike", Text: "Welcome back, everyone."},
|
||||
},
|
||||
}
|
||||
|
||||
output, err := MarkdownRenderer{}.Render(transcript, Options{
|
||||
Title: "Transcript",
|
||||
IncludeTimestamps: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("render markdown: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "# Transcript") {
|
||||
t.Fatalf("expected title in output:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "[00:00:01–00:00:04] **Eric:** Hello there.") {
|
||||
t.Fatalf("expected first segment in output:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "[00:00:05–00:00:08] **Mike:** Welcome back, everyone.") {
|
||||
t.Fatalf("expected second segment in output:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownRendererWithoutTimestamps(t *testing.T) {
|
||||
transcript := Transcript{
|
||||
Segments: []Segment{
|
||||
{ID: 1, Start: 1, End: 4, Speaker: "Eric", Text: "Hello."},
|
||||
},
|
||||
}
|
||||
|
||||
output, err := MarkdownRenderer{}.Render(transcript, Options{
|
||||
Title: "Transcript",
|
||||
IncludeTimestamps: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("render markdown: %v", err)
|
||||
}
|
||||
if strings.Contains(output, "[00:00:01") {
|
||||
t.Fatalf("timestamps should be omitted:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "**Eric:** Hello.") {
|
||||
t.Fatalf("expected speaker/text line:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownRendererWithSegmentIDs(t *testing.T) {
|
||||
transcript := Transcript{
|
||||
Segments: []Segment{
|
||||
{ID: 17, Start: 1, End: 4, Speaker: "Eric", Text: "Hello."},
|
||||
},
|
||||
}
|
||||
|
||||
output, err := MarkdownRenderer{}.Render(transcript, Options{
|
||||
Title: "Transcript",
|
||||
IncludeTimestamps: true,
|
||||
IncludeSegmentIDs: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("render markdown: %v", err)
|
||||
}
|
||||
if !strings.Contains(output, "[#17]") {
|
||||
t.Fatalf("expected segment ID in output:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownRendererMetadataOnlyWhenRequested(t *testing.T) {
|
||||
transcript := Transcript{
|
||||
Schema: "seriatim-full",
|
||||
Metadata: Metadata{
|
||||
Application: "seriatim",
|
||||
Version: "v-test",
|
||||
},
|
||||
}
|
||||
|
||||
withMetadata, err := MarkdownRenderer{}.Render(transcript, Options{
|
||||
Title: "Transcript",
|
||||
IncludeMetadata: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("render with metadata: %v", err)
|
||||
}
|
||||
if !strings.Contains(withMetadata, "- Application: seriatim") {
|
||||
t.Fatalf("expected metadata block:\n%s", withMetadata)
|
||||
}
|
||||
|
||||
withoutMetadata, err := MarkdownRenderer{}.Render(transcript, Options{Title: "Transcript"})
|
||||
if err != nil {
|
||||
t.Fatalf("render without metadata: %v", err)
|
||||
}
|
||||
if strings.Contains(withoutMetadata, "- Application: seriatim") {
|
||||
t.Fatalf("metadata should be omitted:\n%s", withoutMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownRendererCategoryHintItalicsAndUnknownCategories(t *testing.T) {
|
||||
transcript := Transcript{
|
||||
Segments: []Segment{
|
||||
{ID: 1, Start: 1, End: 2, Speaker: "A", Text: "bg", Categories: []string{"background"}},
|
||||
{ID: 2, Start: 2, End: 3, Speaker: "B", Text: "bc", Categories: []string{"backchannel"}},
|
||||
{ID: 3, Start: 3, End: 4, Speaker: "C", Text: "fill", Categories: []string{"filler"}},
|
||||
{ID: 4, Start: 4, End: 5, Speaker: "D", Text: "plain", Categories: []string{"unknown-tag"}},
|
||||
},
|
||||
}
|
||||
|
||||
output, err := MarkdownRenderer{}.Render(transcript, Options{
|
||||
Title: "Transcript",
|
||||
IncludeTimestamps: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("render markdown: %v", err)
|
||||
}
|
||||
if !strings.Contains(output, "**A:** *bg*") {
|
||||
t.Fatalf("expected background italics:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "**B:** *bc*") {
|
||||
t.Fatalf("expected backchannel italics:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "**C:** *fill*") {
|
||||
t.Fatalf("expected filler italics:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "**D:** plain") {
|
||||
t.Fatalf("expected unknown category to be ignored:\n%s", output)
|
||||
}
|
||||
}
|
||||
24
internal/render/model.go
Normal file
24
internal/render/model.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package render
|
||||
|
||||
// Transcript is the render-normalized transcript model used by renderers.
|
||||
type Transcript struct {
|
||||
Schema string
|
||||
Metadata Metadata
|
||||
Segments []Segment
|
||||
}
|
||||
|
||||
// Metadata is the render-relevant artifact metadata.
|
||||
type Metadata struct {
|
||||
Application string
|
||||
Version string
|
||||
}
|
||||
|
||||
// Segment is a normalized render segment.
|
||||
type Segment struct {
|
||||
ID int
|
||||
Start float64
|
||||
End float64
|
||||
Speaker string
|
||||
Text string
|
||||
Categories []string
|
||||
}
|
||||
96
internal/render/normalize.go
Normal file
96
internal/render/normalize.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/seriatim/internal/artifact"
|
||||
)
|
||||
|
||||
// FromOutputArtifact converts a parsed output artifact into the internal render model.
|
||||
func FromOutputArtifact(input artifact.OutputArtifact) (Transcript, error) {
|
||||
switch input.Schema {
|
||||
case artifact.OutputSchemaFull:
|
||||
payload, err := input.FullPayload()
|
||||
if err != nil {
|
||||
return Transcript{}, err
|
||||
}
|
||||
segments := make([]Segment, len(payload.Segments))
|
||||
for index, segment := range payload.Segments {
|
||||
segments[index] = Segment{
|
||||
ID: segment.ID,
|
||||
Start: segment.Start,
|
||||
End: segment.End,
|
||||
Speaker: segment.Speaker,
|
||||
Text: segment.Text,
|
||||
Categories: normalizeCategories(segment.Categories),
|
||||
}
|
||||
}
|
||||
return Transcript{
|
||||
Schema: input.Schema,
|
||||
Metadata: Metadata{
|
||||
Application: payload.Metadata.Application,
|
||||
Version: payload.Metadata.Version,
|
||||
},
|
||||
Segments: segments,
|
||||
}, nil
|
||||
case artifact.OutputSchemaIntermediate:
|
||||
payload, err := input.IntermediatePayload()
|
||||
if err != nil {
|
||||
return Transcript{}, err
|
||||
}
|
||||
segments := make([]Segment, len(payload.Segments))
|
||||
for index, segment := range payload.Segments {
|
||||
segments[index] = Segment{
|
||||
ID: segment.ID,
|
||||
Start: segment.Start,
|
||||
End: segment.End,
|
||||
Speaker: segment.Speaker,
|
||||
Text: segment.Text,
|
||||
Categories: normalizeCategories(segment.Categories),
|
||||
}
|
||||
}
|
||||
return Transcript{
|
||||
Schema: input.Schema,
|
||||
Metadata: Metadata{
|
||||
Application: payload.Metadata.Application,
|
||||
Version: payload.Metadata.Version,
|
||||
},
|
||||
Segments: segments,
|
||||
}, nil
|
||||
case artifact.OutputSchemaMinimal:
|
||||
payload, err := input.MinimalPayload()
|
||||
if err != nil {
|
||||
return Transcript{}, err
|
||||
}
|
||||
segments := make([]Segment, len(payload.Segments))
|
||||
for index, segment := range payload.Segments {
|
||||
segments[index] = Segment{
|
||||
ID: segment.ID,
|
||||
Start: segment.Start,
|
||||
End: segment.End,
|
||||
Speaker: segment.Speaker,
|
||||
Text: segment.Text,
|
||||
Categories: []string{},
|
||||
}
|
||||
}
|
||||
return Transcript{
|
||||
Schema: input.Schema,
|
||||
Metadata: Metadata{
|
||||
Application: payload.Metadata.Application,
|
||||
Version: payload.Metadata.Version,
|
||||
},
|
||||
Segments: segments,
|
||||
}, nil
|
||||
default:
|
||||
return Transcript{}, fmt.Errorf("unsupported artifact schema %q", input.Schema)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCategories(categories []string) []string {
|
||||
if categories == nil {
|
||||
return []string{}
|
||||
}
|
||||
out := make([]string, len(categories))
|
||||
copy(out, categories)
|
||||
return out
|
||||
}
|
||||
139
internal/render/normalize_test.go
Normal file
139
internal/render/normalize_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/seriatim/internal/artifact"
|
||||
"gitea.maximumdirect.net/eric/seriatim/schema"
|
||||
)
|
||||
|
||||
func TestFromOutputArtifactNormalizesSupportedSchemas(t *testing.T) {
|
||||
t.Run("full", func(t *testing.T) {
|
||||
sourceIndex := 0
|
||||
input := schema.Transcript{
|
||||
Metadata: schema.Metadata{
|
||||
Application: "seriatim",
|
||||
Version: "v-test",
|
||||
InputReader: "json-files",
|
||||
InputFiles: []string{"a.json"},
|
||||
PreprocessingModules: []string{"validate-raw"},
|
||||
PostprocessingModules: []string{"assign-ids", "validate-output"},
|
||||
OutputModules: []string{"json"},
|
||||
},
|
||||
Segments: []schema.Segment{
|
||||
{
|
||||
ID: 1,
|
||||
Source: "a.json",
|
||||
SourceSegmentIndex: &sourceIndex,
|
||||
Speaker: "Alice",
|
||||
Start: 1,
|
||||
End: 2,
|
||||
Text: "hello",
|
||||
Categories: []string{"background"},
|
||||
},
|
||||
},
|
||||
OverlapGroups: []schema.OverlapGroup{},
|
||||
}
|
||||
model := mustNormalizeOutputArtifact(t, input)
|
||||
if model.Schema != artifact.OutputSchemaFull {
|
||||
t.Fatalf("schema = %q, want %q", model.Schema, artifact.OutputSchemaFull)
|
||||
}
|
||||
if len(model.Segments) != 1 {
|
||||
t.Fatalf("segment count = %d, want 1", len(model.Segments))
|
||||
}
|
||||
if model.Segments[0].ID != 1 || model.Segments[0].Speaker != "Alice" || model.Segments[0].Text != "hello" {
|
||||
t.Fatalf("unexpected segment: %#v", model.Segments[0])
|
||||
}
|
||||
if len(model.Segments[0].Categories) != 1 || model.Segments[0].Categories[0] != "background" {
|
||||
t.Fatalf("categories = %#v, want [background]", model.Segments[0].Categories)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("intermediate", func(t *testing.T) {
|
||||
input := schema.IntermediateTranscript{
|
||||
Metadata: schema.IntermediateMetadata{
|
||||
Application: "seriatim",
|
||||
Version: "v-test",
|
||||
OutputSchema: artifact.OutputSchemaIntermediate,
|
||||
},
|
||||
Segments: []schema.IntermediateSegment{
|
||||
{ID: 1, Start: 1, End: 2, Speaker: "Alice", Text: "one", Categories: []string{}},
|
||||
{ID: 2, Start: 2, End: 3, Speaker: "Bob", Text: "two"},
|
||||
},
|
||||
}
|
||||
model := mustNormalizeOutputArtifact(t, input)
|
||||
if model.Schema != artifact.OutputSchemaIntermediate {
|
||||
t.Fatalf("schema = %q, want %q", model.Schema, artifact.OutputSchemaIntermediate)
|
||||
}
|
||||
if len(model.Segments[0].Categories) != 0 {
|
||||
t.Fatalf("segment[0] categories = %#v, want empty slice", model.Segments[0].Categories)
|
||||
}
|
||||
if len(model.Segments[1].Categories) != 0 {
|
||||
t.Fatalf("segment[1] categories = %#v, want empty slice", model.Segments[1].Categories)
|
||||
}
|
||||
if model.Segments[0].Categories == nil || model.Segments[1].Categories == nil {
|
||||
t.Fatal("expected non-nil empty categories slices")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("minimal", func(t *testing.T) {
|
||||
input := schema.MinimalTranscript{
|
||||
Metadata: schema.MinimalMetadata{
|
||||
Application: "seriatim",
|
||||
Version: "v-test",
|
||||
OutputSchema: artifact.OutputSchemaMinimal,
|
||||
},
|
||||
Segments: []schema.MinimalSegment{
|
||||
{ID: 1, Start: 1, End: 2, Speaker: "Alice", Text: "one"},
|
||||
},
|
||||
}
|
||||
model := mustNormalizeOutputArtifact(t, input)
|
||||
if model.Schema != artifact.OutputSchemaMinimal {
|
||||
t.Fatalf("schema = %q, want %q", model.Schema, artifact.OutputSchemaMinimal)
|
||||
}
|
||||
if len(model.Segments[0].Categories) != 0 {
|
||||
t.Fatalf("categories = %#v, want empty slice", model.Segments[0].Categories)
|
||||
}
|
||||
if model.Segments[0].Categories == nil {
|
||||
t.Fatal("expected non-nil empty categories slice")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromOutputArtifactRejectsMalformedAndRawInput(t *testing.T) {
|
||||
_, err := artifact.ParseOutputArtifactJSON([]byte(`{"metadata":`))
|
||||
if err == nil {
|
||||
t.Fatal("expected malformed JSON error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "input JSON is malformed") {
|
||||
t.Fatalf("unexpected malformed error: %v", err)
|
||||
}
|
||||
|
||||
rawWhisper := []byte(`{"segments":[{"id":0,"start":0.1,"end":1.2,"text":"hello","words":[{"word":"hello"}]}]}`)
|
||||
_, err = artifact.ParseOutputArtifactJSON(rawWhisper)
|
||||
if err == nil {
|
||||
t.Fatal("expected raw input artifact error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not a valid seriatim output artifact") {
|
||||
t.Fatalf("unexpected raw input error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustNormalizeOutputArtifact(t *testing.T, value any) Transcript {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
parsed, err := artifact.ParseOutputArtifactJSON(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
model, err := FromOutputArtifact(parsed)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize: %v", err)
|
||||
}
|
||||
return model
|
||||
}
|
||||
41
internal/render/registry.go
Normal file
41
internal/render/registry.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package render
|
||||
|
||||
import "fmt"
|
||||
|
||||
const FormatMarkdown = "markdown"
|
||||
|
||||
// Options configures rendering behavior across formats.
|
||||
type Options struct {
|
||||
Title string
|
||||
IncludeTimestamps bool
|
||||
IncludeSegmentIDs bool
|
||||
IncludeMetadata bool
|
||||
}
|
||||
|
||||
// Renderer turns a normalized render model into text output.
|
||||
type Renderer interface {
|
||||
Render(transcript Transcript, opts Options) (string, error)
|
||||
}
|
||||
|
||||
// Registry resolves renderers by public format name.
|
||||
type Registry struct {
|
||||
renderers map[string]Renderer
|
||||
}
|
||||
|
||||
// NewRegistry returns a renderer registry with built-in renderers.
|
||||
func NewRegistry() Registry {
|
||||
return Registry{
|
||||
renderers: map[string]Renderer{
|
||||
FormatMarkdown: MarkdownRenderer{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve resolves a renderer by format name.
|
||||
func (registry Registry) Resolve(format string) (Renderer, error) {
|
||||
renderer, ok := registry.renderers[format]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported --format %q", format)
|
||||
}
|
||||
return renderer, nil
|
||||
}
|
||||
22
internal/render/registry_test.go
Normal file
22
internal/render/registry_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package render
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRegistryResolvesMarkdownRenderer(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
renderer, err := registry.Resolve(FormatMarkdown)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve markdown renderer: %v", err)
|
||||
}
|
||||
if renderer == nil {
|
||||
t.Fatal("expected renderer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryRejectsUnknownRenderer(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
_, err := registry.Resolve("txt")
|
||||
if err == nil {
|
||||
t.Fatal("expected unsupported format error")
|
||||
}
|
||||
}
|
||||
72
internal/render/run.go
Normal file
72
internal/render/run.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.maximumdirect.net/eric/seriatim/internal/artifact"
|
||||
"gitea.maximumdirect.net/eric/seriatim/internal/config"
|
||||
)
|
||||
|
||||
// Run executes artifact-level render orchestration.
|
||||
func Run(ctx context.Context, cfg config.RenderConfig) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cfg.InputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read --input-file %q: %w", cfg.InputFile, err)
|
||||
}
|
||||
|
||||
inputArtifact, err := artifact.ParseOutputArtifactJSON(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("--input-file %q: %w", cfg.InputFile, err)
|
||||
}
|
||||
|
||||
model, err := FromOutputArtifact(inputArtifact)
|
||||
if err != nil {
|
||||
return fmt.Errorf("normalize artifact for render: %w", err)
|
||||
}
|
||||
|
||||
registry := NewRegistry()
|
||||
renderer, err := registry.Resolve(cfg.Format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rendered, err := renderer.Render(model, Options{
|
||||
Title: cfg.Title,
|
||||
IncludeTimestamps: cfg.IncludeTimestamps,
|
||||
IncludeSegmentIDs: cfg.IncludeSegmentIDs,
|
||||
IncludeMetadata: cfg.IncludeMetadata,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("render %q output: %w", cfg.Format, err)
|
||||
}
|
||||
|
||||
if err := writeFile(cfg.OutputFile, rendered); err != nil {
|
||||
return fmt.Errorf("write --output-file %q: %w", cfg.OutputFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFile(path string, content string) (err error) {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create %q: %w", path, err)
|
||||
}
|
||||
defer func() {
|
||||
closeErr := file.Close()
|
||||
if err == nil && closeErr != nil {
|
||||
err = fmt.Errorf("close %q: %w", path, closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := file.WriteString(content); err != nil {
|
||||
return fmt.Errorf("write %q: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user