diff --git a/README.md b/README.md index 8df16f1..5082ba8 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ However, you can also compile the utility manually if Go is [installed](#install Navigate to the project root and run: ```bash -go build main.go +go build -trimpath main.go ``` There may be issues when running `go build` outside of the directory containing `main.go`, @@ -130,4 +130,15 @@ even if the path is specified correctly. This command creates an executable named `embed-code` (or `embed-code.exe` on Windows). For further information, please refer to the [docs](https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies). +Without the `-trimpath` flag, Go includes absolute file paths in stack traces +based on the system where the binary was built. + +Run following command to build binaries for macOS, Windows and Ubuntu: +```bash +mkdir -p bin && \ +GOOS=darwin GOARCH=amd64 go build -trimpath -o bin/embed-code-macos main.go && \ +GOOS=windows GOARCH=amd64 go build -trimpath -o bin/embed-code-windows.exe main.go && \ +GOOS=linux GOARCH=amd64 go build -trimpath -o bin/embed-code-linux main.go +``` + [embed-code-jekyll]: https://github.com/SpineEventEngine/embed-code diff --git a/bin/embed-code-linux b/bin/embed-code-linux new file mode 100755 index 0000000..83a9b25 Binary files /dev/null and b/bin/embed-code-linux differ diff --git a/bin/embed-code-macos b/bin/embed-code-macos index e4a6f67..ac053a5 100755 Binary files a/bin/embed-code-macos and b/bin/embed-code-macos differ diff --git a/bin/embed-code-ubuntu b/bin/embed-code-ubuntu deleted file mode 100755 index dfdfb1f..0000000 Binary files a/bin/embed-code-ubuntu and /dev/null differ diff --git a/bin/embed-code-win.exe b/bin/embed-code-win.exe deleted file mode 100644 index 7aa373e..0000000 Binary files a/bin/embed-code-win.exe and /dev/null differ diff --git a/bin/embed-code-windows.exe b/bin/embed-code-windows.exe new file mode 100755 index 0000000..62b2b2e Binary files /dev/null and b/bin/embed-code-windows.exe differ diff --git a/cli/cli.go b/cli/cli.go index 414bdfa..d265633 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,4 +1,4 @@ -// Copyright 2024, TeamDev. All rights reserved. +// Copyright 2026, TeamDev. All rights reserved. // // Redistribution and use in source and/or binary forms, with or without // modification, must retain the above copyright notice and the following @@ -72,6 +72,8 @@ type Config struct { BaseCodePath string `yaml:"code-path"` BaseDocsPath string `yaml:"docs-path"` EmbedMappings []EmbedMapping `yaml:"embed-mappings"` + Info bool `yaml:"info"` + Stacktrace bool `yaml:"stacktrace"` ConfigPath string Mode string } @@ -82,6 +84,16 @@ type EmbedMapping struct { DocsPath string `yaml:"docs-path"` } +// EmbedCodeSamplesResult is result of the EmbedCodeSamples method. +// +// WriteFragmentFilesResult the result of code fragmentation. +// +// EmbedAllResult the result of embedding code fragments in the documentation. +type EmbedCodeSamplesResult struct { + fragmentation.WriteFragmentFilesResult + embedding.EmbedAllResult +} + const ( ModeCheck = "check" ModeEmbed = "embed" @@ -93,32 +105,28 @@ const ( // // config — a configuration for checking code samples. func CheckCodeSamples(config configuration.Configuration) { - err := fragmentation.WriteFragmentFiles(config) - if err != nil { - panic(err) - } + fragmentation.WriteFragmentFiles(config) embedding.CheckUpToDate(config) } // EmbedCodeSamples embeds code fragments in documentation files. // // config — a configuration for embedding. -func EmbedCodeSamples(config configuration.Configuration) { - err := fragmentation.WriteFragmentFiles(config) - if err != nil { - panic(err) +func EmbedCodeSamples(config configuration.Configuration) EmbedCodeSamplesResult { + fragmentationResult := fragmentation.WriteFragmentFiles(config) + embeddingResult := embedding.EmbedAll(config) + embedding.CheckUpToDate(config) + return EmbedCodeSamplesResult{ + fragmentationResult, + embeddingResult, } - embedding.EmbedAll(config) } // AnalyzeCodeSamples analyzes code fragments in documentation files. // // config — a configuration for embedding. func AnalyzeCodeSamples(config configuration.Configuration) { - err := fragmentation.WriteFragmentFiles(config) - if err != nil { - panic(err) - } + fragmentation.WriteFragmentFiles(config) analyzing.AnalyzeAll(config) fragmentation.CleanFragmentFiles(config) } @@ -142,6 +150,10 @@ func ReadArgs() Config { configPath := flag.String("config-path", "", "a path to a yaml configuration file") mode := flag.String("mode", "", "a mode of embed-code execution, which can be 'check' or 'embed'") + info := flag.Bool("info", false, + "an info-level logging setter that enables info logs when set to 'true'") + stacktrace := flag.Bool("stacktrace", false, + "a stack trace setter that enables stack traces in error logs when set to 'true'") flag.Parse() @@ -155,6 +167,8 @@ func ReadArgs() Config { Separator: *separator, ConfigPath: *configPath, Mode: *mode, + Info: *info, + Stacktrace: *stacktrace, } } @@ -186,6 +200,8 @@ func FillArgsFromConfigFile(args Config) (Config, error) { if isNotEmpty(configFields.Separator) { args.Separator = configFields.Separator } + args.Info = configFields.Info + args.Stacktrace = configFields.Stacktrace return args, nil } diff --git a/embedding/errors.go b/embedding/errors.go index e3747fa..227adce 100644 --- a/embedding/errors.go +++ b/embedding/errors.go @@ -20,8 +20,6 @@ package embedding import ( "fmt" - - "embed-code/embed-code-go/embedding/parsing" ) // UnexpectedDiffError describes an error which occurs if outdated files are found during @@ -33,39 +31,3 @@ type UnexpectedDiffError struct { func (e *UnexpectedDiffError) Error() string { return fmt.Sprintf("unexpected diff: %v", e.changedFiles) } - -// UnexpectedProcessingError describes an error which occurs if something goes wrong -// during embedding. -type UnexpectedProcessingError struct { - Context parsing.Context - initialError error -} - -func (e *UnexpectedProcessingError) Error() string { - errorString := fmt.Sprintf("embedding error for file `%s`: %s.", - e.Context.MarkdownFilePath, e.initialError) - - if len(e.Context.EmbeddingsNotFound) > 0 { - embeddingsNotFoundStr := "\nMissing embeddings: \n" - for _, emb := range e.Context.EmbeddingsNotFound { - embeddingsNotFoundStr += fmt.Sprintf( - "%s — %s\n", - emb.CodeFile, - emb.Fragment) - } - errorString += embeddingsNotFoundStr - } - - if len(e.Context.UnacceptedEmbeddings) > 0 { - unacceptedEmbeddingStr := "\nUnaccepted embeddings: \n" - for _, emb := range e.Context.UnacceptedEmbeddings { - unacceptedEmbeddingStr += fmt.Sprintf( - "%s — %s\n", - emb.CodeFile, - emb.Fragment) - } - errorString += unacceptedEmbeddingStr - } - - return errorString -} diff --git a/embedding/parsing/code_fence_end.go b/embedding/parsing/code_fence_end.go index 25390f5..e28c8ee 100644 --- a/embedding/parsing/code_fence_end.go +++ b/embedding/parsing/code_fence_end.go @@ -19,7 +19,6 @@ package parsing import ( - "fmt" "strings" "embed-code/embed-code-go/configuration" @@ -73,7 +72,7 @@ func (c CodeFenceEndState) Accept(context *Context, _ configuration.Configuratio func renderSample(context *Context) error { content, err := context.EmbeddingInstruction.Content() if err != nil { - return fmt.Errorf("unable to read the code fence end: %s", err.Error()) + return err } for _, line := range content { indentation := strings.Repeat(" ", context.CodeFenceIndentation) diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index f11642e..8f348bf 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -57,6 +57,11 @@ type Context struct { embeddings []parsingContext } +// EmbeddingsCount returns number of found embeddings. +func (c *Context) EmbeddingsCount() int { + return len(c.embeddings) +} + // parsingContext contains the information about the position in the source and the // resulting Markdown files. // diff --git a/embedding/processor.go b/embedding/processor.go index 76177af..1e93b9e 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -1,4 +1,4 @@ -// Copyright 2024, TeamDev. All rights reserved. +// Copyright 2026, TeamDev. All rights reserved. // // Redistribution and use in source and/or binary forms, with or without // modification, must retain the above copyright notice and the following @@ -19,8 +19,11 @@ package embedding import ( + "errors" "fmt" + "log/slog" "os" + "path/filepath" "slices" "strings" @@ -44,6 +47,19 @@ type Processor struct { requiredDocPaths []string } +// EmbedAllResult is result of the EmbedAll method. +// +// TargetFiles is the list of target documentation files. +// +// TotalEmbeddings is the total number of embeddings found in the target documentation files. +// +// UpdatedTargetFiles is the list of updated target documentation files. +type EmbedAllResult struct { + TargetFiles []string + TotalEmbeddings int + UpdatedTargetFiles []string +} + // NewProcessor creates and returns new Processor with given docFile and config. func NewProcessor(docFile string, config configuration.Configuration) Processor { return Processor{ @@ -69,25 +85,24 @@ func NewProcessorWithTransitions(docFile string, config configuration.Configurat // Embed Constructs embedding and modifies the doc file if embedding is needed. // // If any problems faced, an error is returned. -func (p Processor) Embed() error { +func (p Processor) Embed() (*parsing.Context, error) { if !slices.Contains(p.requiredDocPaths, p.DocFilePath) { - return nil + return nil, nil } context, err := p.fillEmbeddingContext() if err != nil { - return &UnexpectedProcessingError{context, err} + return nil, err } - if context.IsContainsEmbedding() && context.IsContentChanged() { data := []byte(strings.Join(context.GetResult(), "\n")) err = os.WriteFile(p.DocFilePath, data, os.FileMode(files.ReadWriteExecPermission)) if err != nil { - return &UnexpectedProcessingError{context, err} + return &context, err } } - return nil + return &context, nil } // FindChangedEmbeddings Returns the list of EmbeddingInstruction that are changed in the @@ -101,7 +116,7 @@ func (p Processor) FindChangedEmbeddings() ([]parsing.Instruction, error) { context, err := p.fillEmbeddingContext() changedEmbeddings := context.FindChangedEmbeddings() if err != nil { - return changedEmbeddings, &UnexpectedProcessingError{context, err} + return changedEmbeddings, err } return changedEmbeddings, nil @@ -126,13 +141,36 @@ func (p Processor) IsUpToDate() bool { // creates an EmbeddingProcessor for each file, and embeds code fragments in them. // // config — a configuration for embedding. -func EmbedAll(config configuration.Configuration) { +func EmbedAll(config configuration.Configuration) EmbedAllResult { requiredDocPaths := requiredDocs(config) + totalEmbeddings := 0 + var updatedTargetFiles []string + var embeddingErrors []error for _, doc := range requiredDocPaths { processor := NewProcessor(doc, config) - if err := processor.Embed(); err != nil { - panic(err) + context, err := processor.Embed() + if err != nil { + embeddingErrors = append(embeddingErrors, err) + continue } + totalEmbeddings += context.EmbeddingsCount() + if context.IsContentChanged() { + updatedTargetFiles = append(updatedTargetFiles, doc) + } + } + slog.Info( + fmt.Sprintf( + "Found `%d` target documentation files with `%d` embeddings under `%s`.", + len(requiredDocPaths), totalEmbeddings, config.DocumentationRoot, + ), + ) + if len(embeddingErrors) > 0 { + panic(errors.Join(embeddingErrors...)) + } + return EmbedAllResult{ + TargetFiles: requiredDocPaths, + TotalEmbeddings: totalEmbeddings, + UpdatedTargetFiles: updatedTargetFiles, } } @@ -149,10 +187,13 @@ func CheckUpToDate(config configuration.Configuration) { // Iterates through the doc file line by line considering them as a states of an embedding. // Such way, transits from the state to the next possible one until it reaches the end of a file. // By the transition process, fills the parsing.Context accordingly, so it is ready to retrieve -// the result. Returns a parsing.Context and an error if any occurs. +// the result. +// +// Returns a parsing.Context and an error if any occurs. func (p Processor) fillEmbeddingContext() (parsing.Context, error) { context := parsing.NewContext(p.DocFilePath) - errorStr := "unable to embed construction for doc file `%s` at line %v: %s" + absDocPath, _ := filepath.Abs(p.DocFilePath) + errorStr := "failed to embed code fragment into doc file `file://%s` at line %v: %s" var currentState parsing.State currentState = parsing.Start @@ -161,14 +202,14 @@ func (p Processor) fillEmbeddingContext() (parsing.Context, error) { for currentState != finishState { accepted, newState, err := p.moveToNextState(¤tState, &context) if err != nil { - return parsing.Context{}, fmt.Errorf(errorStr, p.DocFilePath, context.CurrentIndex(), + return context, fmt.Errorf(errorStr, absDocPath, context.CurrentIndex(), err) } if !accepted { currentState = &parsing.RegularLineState{} context.ResolveUnacceptedEmbedding() - return context, fmt.Errorf(errorStr, p.DocFilePath, context.CurrentIndex(), err) + return context, fmt.Errorf(errorStr, absDocPath, context.CurrentIndex(), err) } currentState = *newState } @@ -228,7 +269,7 @@ func requiredDocs(config configuration.Configuration) []string { return includedDocs } - return removeElements(excludedDocs, includedDocs) + return removeElements(includedDocs, excludedDocs) } func getFilesByPatterns(root string, patterns []string) ([]string, error) { @@ -245,16 +286,16 @@ func getFilesByPatterns(root string, patterns []string) ([]string, error) { return result, nil } -// Removes elements of the second list from the first one. +// Returns the elements of the first array excluding those present in the second array. func removeElements(first, second []string) []string { - firstMap := make(map[string]struct{}) - for _, value := range first { - firstMap[value] = struct{}{} + secondMap := make(map[string]struct{}) + for _, value := range second { + secondMap[value] = struct{}{} } var result []string - for _, value := range second { - if _, exists := firstMap[value]; !exists { + for _, value := range first { + if _, exists := secondMap[value]; !exists { result = append(result, value) } } diff --git a/fragmentation/fragment_file.go b/fragmentation/fragment_file.go index 2678dc2..4d73a9a 100644 --- a/fragmentation/fragment_file.go +++ b/fragmentation/fragment_file.go @@ -93,11 +93,20 @@ func (f FragmentFile) Content() ([]string, error) { return nil, err } - if isPathFileExits { - return files.ReadFile(path) + if !isPathFileExits { + if f.FragmentName != "" { + return nil, fmt.Errorf( + "fragment `%s` from code file `%s` not found", + f.FragmentName, f.CodePath, + ) + } + return nil, fmt.Errorf( + "code file `%s` fragment not found", + f.CodePath, + ) } - return nil, fmt.Errorf("file %s doesn't exist", path) + return files.ReadFile(path) } // Returns string representation of FragmentFile. diff --git a/fragmentation/fragmentation.go b/fragmentation/fragmentation.go index 144f6a0..fa01156 100644 --- a/fragmentation/fragmentation.go +++ b/fragmentation/fragmentation.go @@ -1,4 +1,4 @@ -// Copyright 2024, TeamDev. All rights reserved. +// Copyright 2026, TeamDev. All rights reserved. // // Redistribution and use in source and/or binary forms, with or without // modification, must retain the above copyright notice and the following @@ -39,6 +39,7 @@ import ( "bufio" "embed-code/embed-code-go/files" "fmt" + "log/slog" "os" "path/filepath" @@ -62,6 +63,17 @@ type Fragmentation struct { fragmentBuilders map[string]*FragmentBuilder } +// WriteFragmentFilesResult is result of the WriteFragmentFiles method. +// +// TotalSourceFiles total number of source code files. +// +// TotalFragments is the total number of fragments extracted from the source code files. +// A whole source file also counts as a fragment. +type WriteFragmentFilesResult struct { + TotalSourceFiles int + TotalFragments int +} + // NewFragmentation builds Fragmentation from given codeFileRelative and config. // // codeFileRelative — a relative path to a code file to fragment. @@ -110,11 +122,16 @@ func (f Fragmentation) DoFragmentation() ([]string, map[string]Fragment, error) }(file) scanner := bufio.NewScanner(file) + lineNumber := 0 for scanner.Scan() { + lineNumber++ line := scanner.Text() contentToRender, err = f.parseLine(line, contentToRender) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf( + "failed to do fragmentation on file `file://%s`, line %d: %s", + f.CodeFile, lineNumber, err, + ) } } @@ -132,16 +149,16 @@ func (f Fragmentation) DoFragmentation() ([]string, map[string]Fragment, error) // Keeps the original directory structure relative to the sources root dir. // That is, `SRC/src/main` becomes `OUT/src/main`. // -// Returns an error if the fragmentation couldn't be done. -func (f Fragmentation) WriteFragments() error { +// Returns fragments or an error if the fragmentation couldn't be done. +func (f Fragmentation) WriteFragments() (map[string]Fragment, error) { allLines, fragments, err := f.DoFragmentation() if err != nil { - return err + return nil, err } err = files.EnsureDirExists(f.targetDirectory()) if err != nil { - return err + return nil, err } for _, fragment := range fragments { @@ -149,7 +166,7 @@ func (f Fragmentation) WriteFragments() error { fragment.WriteTo(fragmentFile, allLines, f.Configuration.Separator) } - return nil + return fragments, nil } // WriteFragmentFiles writes each fragment into a corresponding file. @@ -164,23 +181,36 @@ func (f Fragmentation) WriteFragments() error { // config — is a configuration for embedding. // // Returns an error if any of the fragments couldn't be written. -func WriteFragmentFiles(config config.Configuration) error { +func WriteFragmentFiles(config config.Configuration) WriteFragmentFilesResult { includes := config.CodeIncludes codeRoot := config.CodeRoot + totalSourceFiles := 0 + totalFragments := 0 for _, rule := range includes { pattern := fmt.Sprintf("%s/%s", codeRoot, rule) codeFiles, err := doublestar.FilepathGlob(pattern) + totalSourceFiles += len(codeFiles) if err != nil { - return err + panic(err) } for _, codeFile := range codeFiles { - if err = writeFragments(config, codeFile); err != nil { - return err + fragments, err := writeFragments(config, codeFile) + if err != nil { + panic(err) } + totalFragments += len(fragments) } } - return nil + slog.Info( + fmt.Sprintf("Found `%d` source code files with `%d` fragments under `%s`.", + totalSourceFiles, totalFragments, config.CodeRoot), + ) + + return WriteFragmentFilesResult{ + TotalSourceFiles: totalSourceFiles, + TotalFragments: totalFragments, + } } // CleanFragmentFiles deletes Configuration.FragmentsDir if it exists. @@ -198,15 +228,17 @@ func CleanFragmentFiles(config config.Configuration) { } // Checks if the code is able to split into fragments and writes them to a file. -func writeFragments(config config.Configuration, codeFile string) error { +func writeFragments(config config.Configuration, codeFile string) (map[string]Fragment, error) { if shouldDoFragmentation(codeFile) { fragmentation := NewFragmentation(codeFile, config) - if err := fragmentation.WriteFragments(); err != nil { - return err + fragments, err := fragmentation.WriteFragments() + if err != nil { + return nil, err } + return fragments, nil } - return nil + return map[string]Fragment{}, nil } // shouldDoFragmentation reports whether the file is valid to do fragmentation: @@ -238,8 +270,14 @@ func shouldDoFragmentation(filePath string) bool { func (f Fragmentation) parseLine(line string, contentToRender []string) ([]string, error) { cursor := len(contentToRender) - docFragments := FindDocFragments(line) - endDocFragments := FindEndDocFragments(line) + docFragments, startErr := FindDocFragments(line) + if startErr != nil { + return nil, startErr + } + endDocFragments, endErr := FindEndDocFragments(line) + if endErr != nil { + return nil, endErr + } switch { case len(docFragments) > 0: @@ -287,7 +325,7 @@ func (f Fragmentation) parseEndDocFragments(endDocFragments []string, cursor int return err } } else { - return fmt.Errorf("cannot end the fragment `%s` of the file `%s` as it wasn't started", + return fmt.Errorf("cannot end the fragment `%s` of the file `file://%s` as it wasn't started", fragmentName, f.CodeFile) } } diff --git a/fragmentation/lookup.go b/fragmentation/lookup.go index 8ce285c..bc508d7 100644 --- a/fragmentation/lookup.go +++ b/fragmentation/lookup.go @@ -1,4 +1,4 @@ -// Copyright 2024, TeamDev. All rights reserved. +// Copyright 2026, TeamDev. All rights reserved. // // Redistribution and use in source and/or binary forms, with or without // modification, must retain the above copyright notice and the following @@ -19,6 +19,7 @@ package fragmentation import ( + "fmt" "regexp" "strconv" "strings" @@ -37,7 +38,7 @@ const ( // line — a line to search in. // // Returns the list of the names found. -func FindDocFragments(line string) []string { +func FindDocFragments(line string) ([]string, error) { return lookup(line, FragmentStart) } @@ -49,7 +50,7 @@ func FindDocFragments(line string) []string { // line — a line to search in. // // Returns the list of the names found. -func FindEndDocFragments(line string) []string { +func FindEndDocFragments(line string) ([]string, error) { return lookup(line, FragmentEnd) } @@ -62,30 +63,41 @@ func FindEndDocFragments(line string) []string { // // prefix — a user-defined indicator of a fragment, e.g. "#docfragment". // -// Returns the list of the names found. -func lookup(line string, prefix string) []string { +// Returns the list of the names found and error if prefix found without names. +func lookup(line string, prefix string) ([]string, error) { var unquotedNames []string if strings.Contains(line, prefix) { // 1 for trailing space after the prefix. fragmentsStart := strings.Index(line, prefix) + len(prefix) + 1 + if len(line) < fragmentsStart { + return unquotedNames, fmt.Errorf( + "found `%s` pefix without any name", prefix, + ) + } for _, fragmentName := range strings.Split(line[fragmentsStart:], ",") { quotedName := strings.Trim(fragmentName, "\n\t ") - unquotedName := unquoteName(quotedName) + unquotedName, err := unquoteName(quotedName) + if err != nil { + return unquotedNames, err + } unquotedNames = append(unquotedNames, unquotedName) } } - return unquotedNames + return unquotedNames, nil } // Returns the unquoted name from given quotedName. -func unquoteName(quotedName string) string { - r := regexp.MustCompile("\"(.*)\"") +func unquoteName(quotedName string) (string, error) { + r, compilationErr := regexp.Compile("\"(.*)\"") + if compilationErr != nil { + return "", fmt.Errorf("failed to unquote name `%s`: %s", quotedName, compilationErr) + } nameQuoted := r.FindString(quotedName) nameCleaned, err := strconv.Unquote(nameQuoted) if err != nil { - panic(err) + return "", fmt.Errorf("failed to unquote name `%s`: %s", quotedName, err) } - return nameCleaned + return nameCleaned, nil } diff --git a/logging/logger.go b/logging/logger.go new file mode 100644 index 0000000..dd9cfc9 --- /dev/null +++ b/logging/logger.go @@ -0,0 +1,92 @@ +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package logging + +import ( + "fmt" + "golang.org/x/net/context" + "log/slog" + "os" + "runtime/debug" +) + +// Handler is a custom slog.Handler that formats log records for simple console output. +// +// It displays each log message in the format: +// +// HH:MM:SS LEVEL - message +// +// Only messages with level greater than or equal to Handler.Level are printed. +type Handler struct { + Level slog.Level + attributes []slog.Attr +} + +// Enabled returns true if the log level is greater than or equal to the Handler's Level. +func (h *Handler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.Level +} + +// Handle formats the log record and writes it to standard output in a simple readable format: +// +// HH:MM:SS LEVEL - message +func (h *Handler) Handle(_ context.Context, record slog.Record) error { + time := record.Time.Format("15:04:05") + fmt.Printf("%s %s - %s\n", + time, + record.Level.String(), + record.Message, + ) + for _, attr := range h.attributes { + fmt.Printf(" %s=%v\n", attr.Key, attr.Value) + } + + record.Attrs(func(attr slog.Attr) bool { + fmt.Printf(" %s=%v\n", attr.Key, attr.Value) + return true + }) + return nil +} + +// WithAttrs returns a copy of the handler with extra attributes. +func (h *Handler) WithAttrs(attributes []slog.Attr) slog.Handler { + newHandler := *h + newHandler.attributes = append(append([]slog.Attr{}, h.attributes...), attributes...) + return &newHandler +} + +// WithGroup returns a copy of the handler for a new group. +// This handler ignores groups and returns itself. +func (h *Handler) WithGroup(name string) slog.Handler { return h } + +// HandlePanic is a handler for the panic. +// +// To use, defer this function in any method that calls panic +// or invokes other methods that may call panic. +// +// defer HandlePanic(withStacktrace) +func HandlePanic(withStacktrace bool) { + if r := recover(); r != nil { + fmt.Printf("Panic: %v\n", r) + if withStacktrace { + debug.PrintStack() + } + os.Exit(1) + } +} diff --git a/main.go b/main.go index 439d1e7..452886c 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -// Copyright 2024, TeamDev. All rights reserved. +// Copyright 2026, TeamDev. All rights reserved. // // Redistribution and use in source and/or binary forms, with or without // modification, must retain the above copyright notice and the following @@ -20,9 +20,16 @@ package main import ( "embed-code/embed-code-go/cli" + "embed-code/embed-code-go/configuration" + "embed-code/embed-code-go/logging" + "fmt" "log/slog" + "path/filepath" ) +// Version of the embed-code application. +const Version = "1.0.0" + // The entry point for embed-code. // // There are three modes, which are chosen by 'mode' arg. If it is set to 'check', @@ -77,45 +84,91 @@ import ( // - separator — a string which is used as a separator between code fragments. Default value // is "...". func main() { - slog.Info("starting application, reading args...") + fmt.Println(fmt.Sprintf("Running embed-code v%s.", Version)) userArgs := cli.ReadArgs() + configureLogging(userArgs) + defer logging.HandlePanic(userArgs.Stacktrace) if cli.IsUsingConfigFile(userArgs) { err := cli.ValidateConfigFile(userArgs) if err != nil { - slog.Error("the provided config file is not valid.", "error", err) + slog.Error("The provided config file is not valid.", "error", err) return } userArgs, err = cli.FillArgsFromConfigFile(userArgs) if err != nil { - slog.Error("received an issue while reading config file: ", "error", err) + slog.Error("Received an issue while reading config file: ", "error", err) return } } err := cli.ValidateConfig(userArgs) if err != nil { - slog.Error("user arguments are not valid.", "error", err) + slog.Error("User arguments are not valid.", "error", err) return } configs := cli.BuildEmbedCodeConfiguration(userArgs) - for _, config := range configs { - switch userArgs.Mode { - case cli.ModeCheck: - cli.CheckCodeSamples(config) - slog.Info("the documentation files are up-to-date with code files.") - case cli.ModeEmbed: - cli.EmbedCodeSamples(config) + switch userArgs.Mode { + case cli.ModeCheck: + for _, config := range configs { cli.CheckCodeSamples(config) - - slog.Info("the code fragments are successfully embedded.") - case cli.ModeAnalyze: + } + fmt.Println("The documentation files are up-to-date with code files.") + case cli.ModeEmbed: + for _, config := range configs { + embedByConfig(config) + } + fmt.Println("Embedding process finished.") + case cli.ModeAnalyze: + for _, config := range configs { cli.AnalyzeCodeSamples(config) + } + fmt.Println("Analysis is completed, analytics files can be found in /build/analytics folder.") + } +} - slog.Info("analysis is completed, analytics files can be found in /build/analytics folder") +// configureLogging configures the slog logging. +func configureLogging(config cli.Config) { + level := slog.LevelWarn + if config.Info { + level = slog.LevelInfo + } + logger := slog.New(&logging.Handler{Level: level}) + slog.SetDefault(logger) +} + +// embedByConfig runs the cli.EmbedCodeSamples for config and logs the results. +func embedByConfig(config configuration.Configuration) { + result := cli.EmbedCodeSamples(config) + if result.TotalFragments == 0 { + slog.Warn( + fmt.Sprintf( + "No code fragments were found under `%s`.", + config.CodeRoot, + ), + ) + } + if result.TotalEmbeddings == 0 { + slog.Warn( + fmt.Sprintf( + "No embedding placeholders were found under `%s`.", + config.CodeRoot, + ), + ) + } + if len(result.UpdatedTargetFiles) == 0 && + result.TotalFragments != 0 && + result.TotalEmbeddings != 0 { + fmt.Println("All documentation files are already up to date. Nothing to update.") + } + for _, updatedDocFile := range result.UpdatedTargetFiles { + absPath, err := filepath.Abs(updatedDocFile) + if err != nil { + panic(err) } + fmt.Printf("File updated: file://%s.\n", absPath) } }