Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand All @@ -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
Binary file added bin/embed-code-linux
Binary file not shown.
Binary file modified bin/embed-code-macos
Binary file not shown.
Binary file removed bin/embed-code-ubuntu
Binary file not shown.
Binary file removed bin/embed-code-win.exe
Binary file not shown.
Binary file added bin/embed-code-windows.exe
Binary file not shown.
44 changes: 30 additions & 14 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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"
Expand All @@ -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)
}
Expand All @@ -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()

Expand All @@ -155,6 +167,8 @@ func ReadArgs() Config {
Separator: *separator,
ConfigPath: *configPath,
Mode: *mode,
Info: *info,
Stacktrace: *stacktrace,
}
}

Expand Down Expand Up @@ -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
}
Expand Down
38 changes: 0 additions & 38 deletions embedding/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
3 changes: 1 addition & 2 deletions embedding/parsing/code_fence_end.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package parsing

import (
"fmt"
"strings"

"embed-code/embed-code-go/configuration"
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions embedding/parsing/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
85 changes: 63 additions & 22 deletions embedding/processor.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,8 +19,11 @@
package embedding

import (
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
"strings"

Expand All @@ -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{
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
}
}

Expand All @@ -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
Expand All @@ -161,14 +202,14 @@ func (p Processor) fillEmbeddingContext() (parsing.Context, error) {
for currentState != finishState {
accepted, newState, err := p.moveToNextState(&currentState, &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
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
}
Expand Down
Loading