feat(release): beelloo v0.1

This commit is contained in:
2026-02-11 11:09:45 +01:00
parent 75ad1e7cee
commit 0dc9eda240
26 changed files with 1918 additions and 0 deletions

142
internal/cli/run.go Normal file
View File

@ -0,0 +1,142 @@
package cli
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"beelloo/internal/invoice"
"beelloo/internal/render"
"beelloo/internal/server"
)
func Run(args []string, out io.Writer, errOut io.Writer) int {
if len(args) == 0 {
usage(errOut)
return 2
}
switch args[0] {
case "new":
return runNew(args[1:], out, errOut)
case "emit":
return runBuild(args[1:], out, errOut)
case "build":
return runBuild(args[1:], out, errOut)
case "serve":
return runServe(args[1:], out, errOut)
case "-h", "--help", "help":
usage(out)
return 0
default:
fmt.Fprintf(errOut, "unknown command: %s\n", args[0])
usage(errOut)
return 2
}
}
func usage(w io.Writer) {
fmt.Fprintln(w, "Usage:")
fmt.Fprintln(w, " beelloo new <file.md>")
fmt.Fprintln(w, " beelloo build <file.md> [--css=style.css]")
fmt.Fprintln(w, " beelloo serve <file.md> [--addr=127.0.0.1:0] [--css=style.css]")
}
func runNew(args []string, out io.Writer, errOut io.Writer) int {
if len(args) != 1 {
fmt.Fprintln(errOut, "new requires a single file path")
return 2
}
path := args[0]
if err := os.WriteFile(path, []byte(invoice.ScaffoldMarkdown), 0644); err != nil {
fmt.Fprintf(errOut, "failed to write scaffold: %v\n", err)
return 1
}
fmt.Fprintf(out, "scaffold written to %s\n", path)
return 0
}
func runBuild(args []string, out io.Writer, errOut io.Writer) int {
if len(args) < 1 {
fmt.Fprintln(errOut, "build requires a markdown file path")
return 2
}
path := args[0]
cssPath := ""
for _, arg := range args[1:] {
if strings.HasPrefix(arg, "--css=") {
cssPath = strings.TrimPrefix(arg, "--css=")
}
}
file, err := os.Open(path)
if err != nil {
fmt.Fprintf(errOut, "failed to open %s: %v\n", path, err)
return 1
}
defer file.Close()
doc, err := invoice.ParseMarkdown(file)
if err != nil {
fmt.Fprintf(errOut, "parse error: %v\n", err)
return 1
}
if err := invoice.Validate(&doc); err != nil {
fmt.Fprintf(errOut, "validation error: %v\n", err)
return 1
}
html, err := renderWithCSS(doc, cssPath)
if err != nil {
fmt.Fprintf(errOut, "render error: %v\n", err)
return 1
}
outputPath := strings.TrimSuffix(path, filepath.Ext(path)) + ".html"
if err := os.WriteFile(outputPath, []byte(html), 0644); err != nil {
fmt.Fprintf(errOut, "failed to write html: %v\n", err)
return 1
}
fmt.Fprintf(out, "HTML generated at %s\n", outputPath)
return 0
}
func runServe(args []string, out io.Writer, errOut io.Writer) int {
if len(args) < 1 {
fmt.Fprintln(errOut, "serve requires a markdown file path")
return 2
}
path := args[0]
addr := "127.0.0.1:0"
cssPath := ""
for _, arg := range args[1:] {
if strings.HasPrefix(arg, "--addr=") {
addr = strings.TrimPrefix(arg, "--addr=")
}
if strings.HasPrefix(arg, "--css=") {
cssPath = strings.TrimPrefix(arg, "--css=")
}
}
err := server.Serve(server.Config{
Addr: addr,
MDPath: path,
CSSPath: cssPath,
}, out, errOut)
if err != nil {
fmt.Fprintf(errOut, "%v\n", err)
return 1
}
return 0
}
func renderWithCSS(doc invoice.Document, cssPath string) (string, error) {
if cssPath == "" {
return render.RenderHTML(doc)
}
css, err := os.ReadFile(cssPath)
if err != nil {
return "", err
}
return render.RenderHTMLWithCSS(doc, string(css))
}

56
internal/cli/run_test.go Normal file
View File

@ -0,0 +1,56 @@
package cli
import (
"bytes"
"os"
"path/filepath"
"testing"
"beelloo/internal/invoice"
)
func TestRunNewCreatesScaffold(t *testing.T) {
var out bytes.Buffer
var errOut bytes.Buffer
tmp := t.TempDir()
path := filepath.Join(tmp, "invoice.md")
code := Run([]string{"new", path}, &out, &errOut)
if code != 0 {
t.Fatalf("expected exit code 0, got %d: %s", code, errOut.String())
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read scaffold: %v", err)
}
if string(data) != invoice.ScaffoldMarkdown {
t.Fatalf("scaffold content mismatch")
}
}
func TestRunBuildErrors(t *testing.T) {
var out bytes.Buffer
var errOut bytes.Buffer
code := Run([]string{"build"}, &out, &errOut)
if code != 2 {
t.Fatalf("expected exit code 2, got %d", code)
}
code = Run([]string{"build", "missing.md"}, &out, &errOut)
if code != 1 {
t.Fatalf("expected exit code 1 for missing file, got %d", code)
}
}
func TestRunBuildValidationError(t *testing.T) {
var out bytes.Buffer
var errOut bytes.Buffer
tmp := t.TempDir()
path := filepath.Join(tmp, "bad.md")
if err := os.WriteFile(path, []byte("# Facture\n\n## Vendeur\nNom: \n"), 0644); err != nil {
t.Fatalf("write bad markdown: %v", err)
}
code := Run([]string{"build", path}, &out, &errOut)
if code != 1 {
t.Fatalf("expected exit code 1, got %d", code)
}
}