Files
2026-06-08 09:28:03 -05:00

346 lines
8.1 KiB
Go

// Package initwizard contains pure business logic for the interactive init flow.
package initwizard
import (
"fmt"
"path/filepath"
"strings"
"github.com/KofTwentyTwo/license-tool/internal/config"
"github.com/KofTwentyTwo/license-tool/internal/model"
"github.com/KofTwentyTwo/license-tool/internal/render"
"github.com/KofTwentyTwo/license-tool/internal/spdx"
)
// LanguageFamily is the stable label for a source preview sample.
type LanguageFamily string
// Language family labels supported by the source preview catalog.
const (
LanguageTypeScriptJavaScript LanguageFamily = "TypeScript/JavaScript"
LanguagePython LanguageFamily = "Python"
LanguageJava LanguageFamily = "Java"
LanguageGo LanguageFamily = "Go"
LanguageC LanguageFamily = "C"
LanguageCPlusPlus LanguageFamily = "C++"
LanguageCSharp LanguageFamily = "C#"
LanguagePHP LanguageFamily = "PHP"
LanguageRuby LanguageFamily = "Ruby"
LanguageSwift LanguageFamily = "Swift"
LanguageKotlin LanguageFamily = "Kotlin"
LanguageRust LanguageFamily = "Rust"
LanguageShell LanguageFamily = "Shell"
LanguagePowerShell LanguageFamily = "PowerShell"
LanguageR LanguageFamily = "R"
)
// Sample is one deterministic source file the init wizard can show in a preview.
type Sample struct {
Language LanguageFamily
Path string
Source string
}
// SourcePreviewInput is the complete pure input for rendering a sample source
// preview. ResolvedYear is passed in so callers own clocks and git history.
type SourcePreviewInput struct {
Config model.Config
Sample Sample
ResolvedYear string
}
// SourcePreview is a rendered example source file with the generated header applied.
type SourcePreview struct {
Language LanguageFamily
Path string
Content string
}
var catalog = []Sample{
{
Language: LanguageTypeScriptJavaScript,
Path: "example.ts",
Source: strings.TrimLeft(`
export function greet(name: string): string {
return "Hello, " + name;
}
`, "\n"),
},
{
Language: LanguagePython,
Path: "example.py",
Source: strings.TrimLeft(`
def greet(name: str) -> str:
return f"Hello, {name}"
`, "\n"),
},
{
Language: LanguageJava,
Path: "Example.java",
Source: strings.TrimLeft(`
package preview;
public class Example {
public String greet(String name) {
return "Hello, " + name;
}
}
`, "\n"),
},
{
Language: LanguageGo,
Path: "example.go",
Source: strings.TrimLeft(`
package main
func greet(name string) string {
return "Hello, " + name
}
`, "\n"),
},
{
Language: LanguageC,
Path: "example.c",
Source: strings.TrimLeft(`
#include <stdio.h>
void greet(const char *name) {
printf("Hello, %s\n", name);
}
`, "\n"),
},
{
Language: LanguageCPlusPlus,
Path: "example.cpp",
Source: strings.TrimLeft(`
#include <iostream>
void greet(const std::string& name) {
std::cout << "Hello, " << name << std::endl;
}
`, "\n"),
},
{
Language: LanguageCSharp,
Path: "Example.cs",
Source: strings.TrimLeft(`
public class Example
{
public string Greet(string name)
{
return "Hello, " + name;
}
}
`, "\n"),
},
{
Language: LanguagePHP,
Path: "example.php",
Source: strings.TrimLeft(`
<?php
function greet(string $name): string
{
return "Hello, " . $name;
}
`, "\n"),
},
{
Language: LanguageRuby,
Path: "example.rb",
Source: strings.TrimLeft(`
def greet(name)
"Hello, #{name}"
end
`, "\n"),
},
{
Language: LanguageSwift,
Path: "Example.swift",
Source: strings.TrimLeft(`
func greet(_ name: String) -> String {
"Hello, \(name)"
}
`, "\n"),
},
{
Language: LanguageKotlin,
Path: "Example.kt",
Source: strings.TrimLeft(`
package preview
fun greet(name: String): String {
return "Hello, $name"
}
`, "\n"),
},
{
Language: LanguageRust,
Path: "example.rs",
Source: strings.TrimLeft(`
fn greet(name: &str) -> String {
format!("Hello, {}", name)
}
`, "\n"),
},
{
Language: LanguageShell,
Path: "example.sh",
Source: strings.TrimLeft(`
#!/usr/bin/env bash
greet() {
printf 'Hello, %s\n' "$1"
}
`, "\n"),
},
{
Language: LanguagePowerShell,
Path: "example.ps1",
Source: strings.TrimLeft(`
function Get-Greeting {
param([string]$Name)
"Hello, $Name"
}
`, "\n"),
},
{
Language: LanguageR,
Path: "example.R",
Source: strings.TrimLeft(`
greet <- function(name) {
paste("Hello", name)
}
`, "\n"),
},
}
var extensionFamilies = map[string]LanguageFamily{
".ts": LanguageTypeScriptJavaScript,
".tsx": LanguageTypeScriptJavaScript,
".mts": LanguageTypeScriptJavaScript,
".cts": LanguageTypeScriptJavaScript,
".js": LanguageTypeScriptJavaScript,
".jsx": LanguageTypeScriptJavaScript,
".mjs": LanguageTypeScriptJavaScript,
".cjs": LanguageTypeScriptJavaScript,
".py": LanguagePython,
".pyi": LanguagePython,
".java": LanguageJava,
".go": LanguageGo,
".c": LanguageC,
".h": LanguageC,
".cc": LanguageCPlusPlus,
".cpp": LanguageCPlusPlus,
".cxx": LanguageCPlusPlus,
".hh": LanguageCPlusPlus,
".hpp": LanguageCPlusPlus,
".hxx": LanguageCPlusPlus,
".cs": LanguageCSharp,
".php": LanguagePHP,
".phtml": LanguagePHP,
".rb": LanguageRuby,
".swift": LanguageSwift,
".kt": LanguageKotlin,
".kts": LanguageKotlin,
".rs": LanguageRust,
".sh": LanguageShell,
".bash": LanguageShell,
".zsh": LanguageShell,
".ksh": LanguageShell,
".ps1": LanguagePowerShell,
".psm1": LanguagePowerShell,
".r": LanguageR,
}
// Catalog returns the full, stable source preview catalog.
func Catalog() []Sample {
return append([]Sample(nil), catalog...)
}
// SelectSample returns the catalog sample that best matches repo file paths. If no
// catalog language is detected, it returns the C fallback.
func SelectSample(paths []string) Sample {
detected := make(map[LanguageFamily]bool)
for _, path := range paths {
if sample, ok := SampleForPath(path); ok {
detected[sample.Language] = true
}
}
for _, sample := range catalog {
if detected[sample.Language] {
return sample
}
}
return fallbackSample()
}
// SampleForPath maps one repository path to its preview sample when the extension
// belongs to the supported catalog.
func SampleForPath(path string) (Sample, bool) {
ext := strings.ToLower(filepath.Ext(filepath.Base(path)))
if ext == "" {
return Sample{}, false
}
family, ok := extensionFamilies[ext]
if !ok {
return Sample{}, false
}
return sampleForFamily(family)
}
// BuildSourcePreview renders a generated header and inserts it into the selected
// sample source using the same SPDX, filetype, and render behavior as apply.
func BuildSourcePreview(in SourcePreviewInput) (SourcePreview, error) {
sample := in.Sample
if sample.Language == "" {
sample = fallbackSample()
}
if in.ResolvedYear == "" {
return SourcePreview{}, fmt.Errorf("initwizard: resolved year is required")
}
license, ok := spdx.Lookup(in.Config.License)
if !ok {
return SourcePreview{}, fmt.Errorf("initwizard: unknown license %q", in.Config.License)
}
ft, ok := config.LookupFunc(in.Config)(sample.Path)
if !ok {
return SourcePreview{}, fmt.Errorf("initwizard: no file type for sample %q", sample.Path)
}
header, err := render.Header(render.HeaderInput{
License: license,
Holder: in.Config.Holder,
Year: in.ResolvedYear,
Style: in.Config.Style,
FileType: ft,
})
if err != nil {
return SourcePreview{}, err
}
content, _ := render.Insert([]byte(sample.Source), ft, header)
return SourcePreview{
Language: sample.Language,
Path: sample.Path,
Content: string(content),
}, nil
}
// BuildYAMLPreview renders the .license-tool.yaml preview for cfg.
func BuildYAMLPreview(cfg model.Config) ([]byte, error) {
return config.RenderFile(cfg)
}
func fallbackSample() Sample {
sample, _ := sampleForFamily(LanguageC)
return sample
}
func sampleForFamily(family LanguageFamily) (Sample, bool) {
for _, sample := range catalog {
if sample.Language == family {
return sample, true
}
}
return Sample{}, false
}