diff --git a/EMBEDDING.md b/EMBEDDING.md index ce67931..458528a 100644 --- a/EMBEDDING.md +++ b/EMBEDDING.md @@ -209,13 +209,3 @@ The fragments can also be used in other languages: ``` - -### Providing fragments directly - -It is possible to add the desired fragment file with exactly the required content -directly to the fragments folder, for example: `FRAGMENTS/desired-path/MyFile.kt`. - -It can then be embedded just like any other fragment file: -```markdown - -``` diff --git a/README.md b/README.md index e7a8578..f229040 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,7 @@ The available arguments are: * `-code-path`: (Optional) Path to the source code root directory. * `-docs-path`: (Optional) Path to the documentation root directory. * `-config-path`: (Optional) Path to a YAML configuration file containing `code-path` and `docs-path`. - * `-code-includes`: (Optional) Comma-separated glob patterns for source files to include (e.g., `"**/*.java,**/*.gradle"`). Defaults to `"**/*.*"`. - * `-code-excludes`: (Optional) Comma-separated glob patterns for source files to exclude. * `-doc-includes`: (Optional) Comma-separated glob patterns for documentation files to include. Defaults to `"**/*.md,**/*.html"`. - * `-fragments-path`: (Optional) Directory for storing code fragments. Defaults to `./build/fragments`. * `-separator`: (Optional) String used to separate joined code fragments. Defaults to `...`. Even though the `code-path`, `docs-path`, and `config-path` arguments are optional, @@ -87,7 +84,6 @@ Optional settings can be defined in a YAML configuration file: ```yaml code-path: path/to/code/root docs-path: path/to/docs/root -code-includes: "**/*.java,**/*.gradle" doc-excludes: "**/*-old.*,**/deprecated/*.*" ``` @@ -98,7 +94,6 @@ embeddings: - name: java code-path: path/to/code/root/java docs-path: path/to/java/docs - code-includes: "**/*.java" - name: kotlin code-path: - name: samples @@ -131,20 +126,15 @@ The available fields for the configuration file are: ``` **Do not forget the dollar sign (`$`) before the path name.** - It is possible to specify a path without a name or with an empty name. - In this case, fragments will be stored in the root defined by `fragments-path`. - - It is also possible to specify multiple paths with the same name, - but this may lead to fragments being overwritten if they have the same relative path and name. + Code source names must be unique. A configuration may use either one unnamed + code source or one or more named code sources, but named and unnamed sources + cannot be mixed. * `docs-path`: (Mandatory) Path to the documentation root. - * `code-includes`: (Optional) Glob patterns for source files to include. - It may be represented as a comma-separated string list or as a YAML sequence. * `doc-excludes`: (Optional) Glob patterns for documentation files to exclude. It may be represented as a comma-separated string list or as a YAML sequence. * `doc-includes`: (Optional) Glob patterns for documentation files to include. It may be represented as a comma-separated string list or as a YAML sequence. - * `fragments-path`: (Optional) Directory for code fragments. * `separator`: (Optional) Separator for fragments. * `embeddings`: (Optional) A list of complete embedding configurations for multiple documentation targets. When `embeddings` is set, do not set root-level `code-path` diff --git a/cli/cli.go b/cli/cli.go index a7dd473..ac46d41 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -22,13 +22,11 @@ import ( _type "embed-code/embed-code-go/type" "flag" "os" - "path/filepath" "strings" "embed-code/embed-code-go/analyzing" "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding" - "embed-code/embed-code-go/fragmentation" "gopkg.in/yaml.v3" ) @@ -39,12 +37,6 @@ import ( // // BaseDocsPath — a path to a root directory with docs files. // -// CodeIncludes — a StringList with patterns for filtering the code files -// to be considered. -// Directories are never matched by these patterns. -// For example, "**/*.java,**/*.gradle". -// The default value is "**/*.*". -// // DocIncludes — a StringList with patterns for filtering files // in which we should look for embedding instructions. // The patterns are resolved relatively to the `documentation_root`. @@ -55,9 +47,6 @@ import ( // DocExcludes - a StringList with patterns for filtering documentation files // which should be excluded from the embedding process. // -// FragmentsPath — a directory where fragmented code is stored. A temporary directory that should -// not be tracked in VCS. The default value is: "./build/fragments". -// // Separator — a string that's inserted between multiple partitions of a single fragment. // The default value is "...". // @@ -73,10 +62,8 @@ import ( type Config struct { BaseCodePaths _type.NamedPathList `yaml:"code-path"` BaseDocsPath string `yaml:"docs-path"` - CodeIncludes _type.StringList `yaml:"code-includes"` DocIncludes _type.StringList `yaml:"doc-includes"` DocExcludes _type.StringList `yaml:"doc-excludes"` - FragmentsPath string `yaml:"fragments-path"` Separator string `yaml:"separator"` Embeddings []EmbeddingConfig `yaml:"embeddings"` Info bool `yaml:"info"` @@ -87,23 +74,18 @@ type Config struct { // EmbeddingConfig contains a complete configuration for one embedding target. type EmbeddingConfig struct { - Name string `yaml:"name"` - CodePaths _type.NamedPathList `yaml:"code-path"` - DocsPath string `yaml:"docs-path"` - CodeIncludes _type.StringList `yaml:"code-includes"` - DocIncludes _type.StringList `yaml:"doc-includes"` - DocExcludes _type.StringList `yaml:"doc-excludes"` - FragmentsPath string `yaml:"fragments-path"` - Separator string `yaml:"separator"` + Name string `yaml:"name"` + CodePaths _type.NamedPathList `yaml:"code-path"` + DocsPath string `yaml:"docs-path"` + DocIncludes _type.StringList `yaml:"doc-includes"` + DocExcludes _type.StringList `yaml:"doc-excludes"` + Separator string `yaml:"separator"` } // 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 } @@ -113,24 +95,19 @@ const ( ModeAnalyze = "analyze" ) -// CheckCodeSamples checks documentation to be up-to-date with code files. Raises -// UnexpectedDiffError if not. +// CheckCodeSamples returns documentation files that are not up-to-date with code files. // // config — a configuration for checking code samples. -func CheckCodeSamples(config configuration.Configuration) { - fragmentation.WriteFragmentFiles(config) - embedding.CheckUpToDate(config) +func CheckCodeSamples(config configuration.Configuration) []string { + return embedding.CheckUpToDate(config) } // EmbedCodeSamples embeds code fragments in documentation files. // // config — a configuration for embedding. func EmbedCodeSamples(config configuration.Configuration) EmbedCodeSamplesResult { - fragmentationResult := fragmentation.WriteFragmentFiles(config) embeddingResult := embedding.EmbedAll(config) - embedding.CheckUpToDate(config) return EmbedCodeSamplesResult{ - fragmentationResult, embeddingResult, } } @@ -139,9 +116,7 @@ func EmbedCodeSamples(config configuration.Configuration) EmbedCodeSamplesResult // // config — a configuration for embedding. func AnalyzeCodeSamples(config configuration.Configuration) { - fragmentation.WriteFragmentFiles(config) analyzing.AnalyzeAll(config) - fragmentation.CleanFragmentFiles(config) } // ReadArgs reads user-specified args from the command line. @@ -150,14 +125,10 @@ func AnalyzeCodeSamples(config configuration.Configuration) { func ReadArgs() Config { codePath := flag.String("code-path", "", "a path to a root directory with code files") docsPath := flag.String("docs-path", "", "a path to a root directory with docs files") - codeIncludes := flag.String("code-includes", "", - "a comma-separated string of glob patterns for code files to include") docIncludes := flag.String("doc-includes", "", "a comma-separated string of glob patterns for docs files to include") docExcludes := flag.String("doc-excludes", "", "a comma-separated string of glob patterns for docs files to exclude") - fragmentsPath := flag.String("fragments-path", "", - "a path to a directory where fragmented code is stored") separator := flag.String("separator", "", "a string that's inserted between multiple partitions of a single fragment") configPath := flag.String("config-path", "", "a path to a yaml configuration file") @@ -173,10 +144,8 @@ func ReadArgs() Config { return Config{ BaseCodePaths: _type.NamedPathList{_type.NamedPath{Path: *codePath}}, BaseDocsPath: *docsPath, - CodeIncludes: parseListArgument(*codeIncludes), DocIncludes: parseListArgument(*docIncludes), DocExcludes: parseListArgument(*docExcludes), - FragmentsPath: *fragmentsPath, Separator: *separator, ConfigPath: *configPath, Mode: *mode, @@ -195,9 +164,6 @@ func FillArgsFromConfigFile(args Config) (Config, error) { args.BaseDocsPath = configFields.BaseDocsPath args.BaseCodePaths = configFields.BaseCodePaths - if len(configFields.CodeIncludes) > 0 { - args.CodeIncludes = configFields.CodeIncludes - } if len(configFields.Embeddings) > 0 { args.Embeddings = configFields.Embeddings } @@ -207,9 +173,6 @@ func FillArgsFromConfigFile(args Config) (Config, error) { if len(configFields.DocExcludes) > 0 { args.DocExcludes = configFields.DocExcludes } - if isNotEmpty(configFields.FragmentsPath) { - args.FragmentsPath = configFields.FragmentsPath - } if isNotEmpty(configFields.Separator) { args.Separator = configFields.Separator } @@ -248,19 +211,12 @@ func configFromEmbedding(embedding EmbeddingConfig) configuration.Configuration embedCodeConfig.CodeRoots = embedding.CodePaths embedCodeConfig.DocumentationRoot = embedding.DocsPath - if len(embedding.CodeIncludes) > 0 { - embedCodeConfig.CodeIncludes = embedding.CodeIncludes - } if len(embedding.DocIncludes) > 0 { embedCodeConfig.DocIncludes = embedding.DocIncludes } if len(embedding.DocExcludes) > 0 { embedCodeConfig.DocExcludes = embedding.DocExcludes } - if isNotEmpty(embedding.FragmentsPath) { - embedCodeConfig.FragmentsDir = embedding.FragmentsPath - } - embedCodeConfig.FragmentsDir = filepath.Join(embedCodeConfig.FragmentsDir, embedding.Name) if isNotEmpty(embedding.Separator) { embedCodeConfig.Separator = embedding.Separator } @@ -272,15 +228,9 @@ func configFromEmbedding(embedding EmbeddingConfig) configuration.Configuration func configWithOptionalParams(userArgs Config) configuration.Configuration { embedCodeConfig := configuration.NewConfiguration() - if len(userArgs.CodeIncludes) > 0 { - embedCodeConfig.CodeIncludes = userArgs.CodeIncludes - } if len(userArgs.DocIncludes) > 0 { embedCodeConfig.DocIncludes = userArgs.DocIncludes } - if isNotEmpty(userArgs.FragmentsPath) { - embedCodeConfig.FragmentsDir = userArgs.FragmentsPath - } if isNotEmpty(userArgs.Separator) { embedCodeConfig.Separator = userArgs.Separator } diff --git a/cli/cli_test.go b/cli/cli_test.go index 726acc6..0d123a2 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -22,7 +22,6 @@ package cli_test import ( "embed-code/embed-code-go/cli" - "embed-code/embed-code-go/configuration" _type "embed-code/embed-code-go/type" "os" "path/filepath" @@ -76,20 +75,6 @@ var _ = Describe("CLI validation", func() { Expect(cli.ValidateConfig(config)).Error().ShouldNot(HaveOccurred()) }) - It("should store embedding fragments under a named subfolder", func() { - embedding := baseEmbeddingConfig() - embedding.FragmentsPath = "/tmp/fragments" - config := cli.Config{ - Mode: cli.ModeCheck, - Embeddings: []cli.EmbeddingConfig{embedding}, - } - - embedConfigs := cli.BuildEmbedCodeConfiguration(config) - - Expect(embedConfigs).To(HaveLen(1)) - Expect(embedConfigs[0].Name).To(Equal("docs")) - Expect(embedConfigs[0].FragmentsDir).To(Equal(filepath.Join("/tmp/fragments", "docs"))) - }) }) Context("with invalid config", func() { @@ -155,9 +140,9 @@ var _ = Describe("CLI validation", func() { It("should fail validation when embeddings and root optional params are set at the same time", func() { invalidConfig := cli.Config{ - Mode: cli.ModeCheck, - CodeIncludes: []string{"**/*.java"}, - Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + Mode: cli.ModeCheck, + DocIncludes: []string{"**/*.md"}, + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, } Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) @@ -190,6 +175,42 @@ var _ = Describe("CLI validation", func() { "duplicate embedding names detected:\n- docs")) }) + It("should fail validation when source code path names are duplicated", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Name: "samples", Path: codeResourcePath("java")}, + _type.NamedPath{Name: "samples", Path: codeResourcePath("kotlin")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "duplicate source code path names detected:\n- samples")) + }) + + It("should fail validation when multiple unnamed sources code paths are configured", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Path: codeResourcePath("java")}, + _type.NamedPath{Path: codeResourcePath("kotlin")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "only one unnamed source code path is allowed")) + }) + + It("should fail validation when named and unnamed source code paths are mixed", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Name: "java", Path: codeResourcePath("java")}, + _type.NamedPath{Path: codeResourcePath("kotlin")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "named and unnamed source code paths cannot be mixed")) + }) + It("should correctly convert embeddings to a few configs", func() { config := cli.Config{ Mode: cli.ModeCheck, @@ -204,18 +225,12 @@ var _ = Describe("CLI validation", func() { Expect(embedConfigs[0].Name).To(Equal("java")) Expect(embedConfigs[0].CodeRoots[0].Path).To(Equal("test/resources/code/java")) Expect(embedConfigs[0].DocumentationRoot).To(Equal("test/resources/docs")) - Expect(embedConfigs[0].FragmentsDir).To(Equal( - filepath.Join(configuration.DefaultFragmentsDir, "java"))) Expect(embedConfigs[1].Name).To(Equal("kotlin")) Expect(embedConfigs[1].CodeRoots[0].Path).To(Equal("test/resources/code/kotlin")) Expect(embedConfigs[1].DocumentationRoot).To(Equal("test/resources/docs/nested-dir-1")) - Expect(embedConfigs[1].FragmentsDir).To(Equal( - filepath.Join(configuration.DefaultFragmentsDir, "kotlin"))) Expect(embedConfigs[2].Name).To(Equal("nested-java")) Expect(embedConfigs[2].DocumentationRoot).To( Equal("test/resources/docs/nested-dir-1/nested-dir-3")) - Expect(embedConfigs[2].FragmentsDir).To(Equal( - filepath.Join(configuration.DefaultFragmentsDir, "nested-java"))) Expect(embedConfigs[2].Separator).To(Equal("---")) }) @@ -255,3 +270,14 @@ func configFilePath() string { return parentDir + "/test/resources/config_files/correct_config.yml" } + +// codeResourcePath builds an absolute path to a test source-code fixture directory. +func codeResourcePath(name string) string { + currentDir, err := os.Getwd() + if err != nil { + panic(err) + } + parentDir := filepath.Dir(currentDir) + + return filepath.Join(parentDir, "test/resources/code", name) +} diff --git a/cli/cli_validation.go b/cli/cli_validation.go index 3880e0b..5b8bd8c 100644 --- a/cli/cli_validation.go +++ b/cli/cli_validation.go @@ -110,7 +110,7 @@ func validateConfig(config Config) error { if err != nil { return err } - err = findCodeSourceDuplications(config.BaseCodePaths) + err = validateCodeSources(config.BaseCodePaths) if err != nil { return err } @@ -118,11 +118,6 @@ func validateConfig(config Config) error { if err != nil { return err } - _, err = validatePathSet(config.FragmentsPath) - if err != nil { - return err - } - isRootsSet := isCodePathsSet && isDocsPathSet isOneOfRootsSet := isCodePathsSet || isDocsPathSet @@ -179,7 +174,7 @@ func validateEmbeddingConfig(embedding EmbeddingConfig, index int) error { if err != nil { return fmt.Errorf("embedding `%s`: %w", embedding.Name, err) } - if err = findCodeSourceDuplications(embedding.CodePaths); err != nil { + if err = validateCodeSources(embedding.CodePaths); err != nil { return fmt.Errorf("embedding `%s`: %w", embedding.Name, err) } @@ -187,11 +182,6 @@ func validateEmbeddingConfig(embedding EmbeddingConfig, index int) error { if err != nil { return fmt.Errorf("embedding `%s`: %w", embedding.Name, err) } - _, err = validatePathSet(embedding.FragmentsPath) - if err != nil { - return fmt.Errorf("embedding `%s`: %w", embedding.Name, err) - } - isRootsSet := isCodePathsSet && isDocsPathSet if !isRootsSet { return fmt.Errorf("embedding `%s`: `code-path` and `docs-path` must both be set", @@ -256,14 +246,11 @@ func verifyDuplicateEmbeddingDocsPaths(embeddings []EmbeddingConfig) { // validateOptionalParamsSet reports whether at least one optional config is set. func validateOptionalParamsSet(config Config) bool { - isCodeIncludesSet := len(config.CodeIncludes) > 0 isDocIncludesSet := len(config.DocIncludes) > 0 isDocExcludesSet := len(config.DocExcludes) > 0 isSeparatorSet := isNotEmpty(config.Separator) - isFragmentPathSet := isNotEmpty(config.FragmentsPath) - return isCodeIncludesSet || isDocIncludesSet || isFragmentPathSet || - isSeparatorSet || isDocExcludesSet + return isDocIncludesSet || isSeparatorSet || isDocExcludesSet } // validatePathSet reports whether path is set and checks if it exists. @@ -312,63 +299,79 @@ func validatePaths(paths _type.NamedPathList) (bool, error) { return allPathsSet, nil } -// findCodeSourceDuplications checks the provided code sources for duplicate names and paths. -// -// It logs a warning for duplicate names and returns an error for duplicate paths. -func findCodeSourceDuplications(paths _type.NamedPathList) error { - nameDuplicates := make(map[string][]string) +// validateCodeSources checks that code sources can be resolved unambiguously. +func validateCodeSources(paths _type.NamedPathList) error { + nameCount := make(map[string]int) pathCount := make(map[string]int) + pathNames := make(map[string][]string) + unnamedCount := 0 + hasNamed := false for _, p := range paths { - name := p.Name - if isEmpty(name) { - name = "(unnamed)" + if isEmpty(p.Path) { + continue + } + if isEmpty(p.Name) { + unnamedCount++ + } else { + hasNamed = true + nameCount[p.Name]++ } - nameDuplicates[name] = append(nameDuplicates[name], p.Path) pathCount[p.Path]++ + pathNames[p.Path] = append(pathNames[p.Path], p.Name) } - verifyDuplicateNames(nameDuplicates) - return verifyDuplicatePaths(pathCount) + if err := verifyCodeSourceNames(nameCount); err != nil { + return err + } + if unnamedCount > 1 { + return errors.New("only one unnamed source code path is allowed") + } + if hasNamed && unnamedCount > 0 { + return errors.New("named and unnamed source code paths cannot be mixed") + } + + warnDuplicatePaths(pathCount, pathNames) + + return nil } -// verifyDuplicateNames logs a warning if multiple code sources share the same name. -func verifyDuplicateNames(nameDuplicates map[string][]string) { - var warnLines []string - for name, ps := range nameDuplicates { - if len(ps) > 1 { - warnLines = append(warnLines, "- "+name) - for _, path := range ps { - warnLines = append(warnLines, " - "+path) - } +// verifyCodeSourceNames returns an error if multiple code sources share the same name. +func verifyCodeSourceNames(nameCount map[string]int) error { + var errLines []string + for name, count := range nameCount { + if count > 1 { + errLines = append(errLines, "- "+name) } } - if len(warnLines) > 0 { - slog.Warn( - "Duplicate code source names detected, it may lead to " + - "overwriting code fragments with the same relative path:\n" + - strings.Join(warnLines, "\n"), + if len(errLines) > 0 { + slices.Sort(errLines) + return fmt.Errorf( + "duplicate source code path names detected:\n%s", + strings.Join(errLines, "\n"), ) } + return nil } -// verifyDuplicatePaths returns an error if multiple code sources use the same path. -func verifyDuplicatePaths(pathCount map[string]int) error { - var errLines []string +// warnDuplicatePaths logs a warning if multiple code sources use the same path. +func warnDuplicatePaths(pathCount map[string]int, pathNames map[string][]string) { + var warnLines []string for path, count := range pathCount { if count > 1 { - errLines = append(errLines, "- "+path) + names := pathNames[path] + slices.Sort(names) + warnLines = append(warnLines, fmt.Sprintf("- %s: %s", path, strings.Join(names, ", "))) } } - if len(errLines) > 0 { - return fmt.Errorf( - "duplicate code source paths detected:\n%s", - strings.Join(errLines, "\n"), + if len(warnLines) > 0 { + slices.Sort(warnLines) + slog.Warn( + "Duplicate source code paths detected:\n" + strings.Join(warnLines, "\n"), ) } - return nil } // isNotEmpty reports whether the given string is not empty. diff --git a/configuration/configuration.go b/configuration/configuration.go index 133a533..c62484a 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -24,25 +24,17 @@ import ( ) const ( - DefaultSeparator = "..." - DefaultFragmentsDir = "./build/fragments" + DefaultSeparator = "..." ) -var DefaultInclude = []string{"**/*.*"} var DefaultDocIncludes = []string{"**/*.md", "**/*.html"} // Configuration contains the settings for the plugin to work. // -// It is used to get data for scanning for and doc files, to receive fragments' dir and separator -// for partitions. +// It is used to get data for scanning docs and resolving source files. // The example of creating the Configuration with default values: // // var config = configuration.NewConfiguration() -// -// If there's need to modify the default configuration, -// it can be done with just setting values to corresponding fields: -// -// config.FragmentsDir = "foo/bar" type Configuration struct { // Name identifies this configuration when it is built from an embeddings entry. Name string @@ -53,15 +45,6 @@ type Configuration struct { // DocumentationRoot is a root directory of the documentation files. DocumentationRoot string - // CodeIncludes is a list of patterns for filtering the code files to be considered. - // - // Directories are never matched by these patterns. - // - // For example, ["**/*.java", "**/*.gradle"]. - // - // The default value is "**/*". - CodeIncludes []string - // DocIncludes is a list of patterns for filtering files in which we should look for embedding // instructions. // @@ -86,12 +69,6 @@ type Configuration struct { // Be the default, it is not set. DocExcludes []string - // FragmentsDir is a directory where fragmented code is stored. A temporary directory that - // should not be tracked in VCS. - // - // The default value is: "./build/fragments". - FragmentsDir string - // Separator is a string that's inserted between multiple partitions of a single fragment. // // The default value is: "..." (three dots). @@ -101,9 +78,7 @@ type Configuration struct { // NewConfiguration builds the default config. func NewConfiguration() Configuration { return Configuration{ - CodeIncludes: DefaultInclude, - DocIncludes: DefaultDocIncludes, - FragmentsDir: DefaultFragmentsDir, - Separator: DefaultSeparator, + DocIncludes: DefaultDocIncludes, + Separator: DefaultSeparator, } } diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index 0463ef7..e498210 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -54,7 +54,7 @@ var _ = Describe("Embedding", func() { if err != nil { Fail("unexpected error during the test setup: " + err.Error()) } - config = buildConfigWithPreparedFragments() + config = buildConfigWithSourceFiles() // Copying files not to edit them directly during the test run. copyDirRecursive("../test/resources/docs", config.DocumentationRoot) @@ -107,6 +107,42 @@ var _ = Describe("Embedding", func() { Expect(processor.IsUpToDate()).Should(BeTrue()) }) + It("should embed directly from source", func() { + docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) + processor := embedding.NewProcessor(docPath, config) + + Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) + + Expect(processor.IsUpToDate()).Should(BeTrue()) + }) + + It("should report files that are not up to date", func() { + config.DocIncludes = []string{"doc.md"} + docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) + + Expect(embedding.CheckUpToDate(config)).Should(ContainElement(docPath)) + }) + + It("should report all check errors", func() { + config.DocIncludes = []string{"missing-closing-tag.md", "unclosed-nested-tag.md"} + + var recovered any + func() { + defer func() { + recovered = recover() + }() + embedding.CheckUpToDate(config) + }() + + Expect(recovered).ShouldNot(BeNil()) + Expect(fmt.Sprint(recovered)).Should(And( + ContainSubstring("missing-closing-tag.md"), + ContainSubstring("the `` tag is not closed"), + ContainSubstring("unclosed-nested-tag.md"), + ContainSubstring("element closed by "), + )) + }) + It("should embed with multi lined tag attributes", func() { docPath := fmt.Sprintf("%s/multi-lined-valid-tag-attributes.md", config.DocumentationRoot) processor := embedding.NewProcessor(docPath, config) @@ -160,7 +196,7 @@ var _ = Describe("Embedding", func() { Expect(processor.IsUpToDate()).Should(BeTrue()) }) - It("should not embed to a file matched the `code-excludes` pattern", func() { + It("should not embed to a file matched the `doc-excludes` pattern", func() { config.DocExcludes = []string{"**/excluded-doc.*"} docPath := fmt.Sprintf("%s/excluded-doc.md", config.DocumentationRoot) @@ -171,11 +207,10 @@ var _ = Describe("Embedding", func() { }) }) -func buildConfigWithPreparedFragments() configuration.Configuration { +func buildConfigWithSourceFiles() configuration.Configuration { var config = configuration.NewConfiguration() config.DocumentationRoot = temporaryTestDir - config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code"}} - config.FragmentsDir = "../test/resources/prepared-fragments" + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} return config } diff --git a/embedding/errors.go b/embedding/errors.go deleted file mode 100644 index 227adce..0000000 --- a/embedding/errors.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024, 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 embedding - -import ( - "fmt" -) - -// UnexpectedDiffError describes an error which occurs if outdated files are found during -// the checking. -type UnexpectedDiffError struct { - changedFiles []string -} - -func (e *UnexpectedDiffError) Error() string { - return fmt.Sprintf("unexpected diff: %v", e.changedFiles) -} diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index 1033eb3..76cc87f 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -100,25 +100,15 @@ func NewInstruction( // // Returns an error if there was an error during reading the content. func (e Instruction) Content() ([]string, error) { - fragmentName := e.Fragment - if fragmentName == "" { - fragmentName = fragmentation.DefaultFragmentName - } - file := fragmentation.FragmentFile{ - CodePath: e.CodeFile, - FragmentName: fragmentName, - Configuration: e.Configuration, + fileContent, err := fragmentation.ResolveContent(e.CodeFile, e.Fragment, e.Configuration) + if err != nil { + return nil, err } if e.StartPattern != nil || e.EndPattern != nil { - fileContent, err := file.Content() - if err != nil { - return nil, err - } - return e.matchingLines(fileContent), nil } - return file.Content() + return fileContent, nil } // Returns string representation of Instruction. diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index b70bad9..c4183f6 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -56,7 +56,7 @@ var _ = Describe("Instruction", func() { if err != nil { Fail("unexpected error during the test setup: " + err.Error()) } - config = buildConfigWithPreparedFragments() + config = buildConfigWithSourceFiles() }) It("should have an error while parsing malformed XML string", func() { @@ -82,7 +82,7 @@ var _ = Describe("Instruction", func() { Expect(parsing.FromXML(xmlString, config)).Error().ShouldNot(HaveOccurred()) }) - It("should successfully read fragments directory", func() { + It("should successfully read source content", func() { instructionParams := TestInstructionParams{ closeTag: true, } @@ -298,11 +298,10 @@ func getXMLExtractionContent(fileName string, params TestInstructionParams, return readInstructionContent(instruction) } -func buildConfigWithPreparedFragments() configuration.Configuration { +func buildConfigWithSourceFiles() configuration.Configuration { var config = configuration.NewConfiguration() config.DocumentationRoot = "../../test/resources/docs" - config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../../test/resources/code"}} - config.FragmentsDir = "../../test/resources/prepared-fragments" + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../../test/resources/code/java"}} return config } diff --git a/embedding/processor.go b/embedding/processor.go index b612947..4c08300 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -124,15 +124,25 @@ func (p Processor) FindChangedEmbeddings() ([]parsing.Instruction, error) { // IsUpToDate reports whether the embedding of the target markdown is up-to-date with the code file. func (p Processor) IsUpToDate() bool { + upToDate, err := p.isUpToDate() + if err != nil { + panic(err) + } + + return upToDate +} + +// isUpToDate reports whether the target markdown is up-to-date and returns processing errors. +func (p Processor) isUpToDate() (bool, error) { if !slices.Contains(p.requiredDocPaths, p.DocFilePath) { - return true + return true, nil } context, err := p.fillEmbeddingContext() if err != nil { - panic(err) + return false, err } - return !context.IsContentChanged() + return !context.IsContentChanged(), nil } // EmbedAll processes embedding for multiple documentation files based on provided config. @@ -189,14 +199,16 @@ func configNameLabel(config configuration.Configuration) string { return fmt.Sprintf(" for embedding `%s`", config.Name) } -// CheckUpToDate raises an error if the documentation files are not up-to-date with code files. +// CheckUpToDate returns documentation files that are not up-to-date with code files. // // config — a configuration for embedding. -func CheckUpToDate(config configuration.Configuration) { - changedFiles := findChangedFiles(config) - if len(changedFiles) > 0 { - panic(UnexpectedDiffError{changedFiles}) +func CheckUpToDate(config configuration.Configuration) []string { + changedFiles, checkErrors := findChangedFiles(config) + if len(checkErrors) > 0 { + panic(errors.Join(checkErrors...)) } + + return changedFiles } // Iterates through the doc file line by line considering them as a states of an embedding. @@ -265,17 +277,22 @@ func (p Processor) moveToNextState(state *parsing.State, context *parsing.Contex // Returns a list of documentation files that are not up-to-date with their code files. // // config — a configuration for embedding. -func findChangedFiles(config configuration.Configuration) []string { +func findChangedFiles(config configuration.Configuration) ([]string, []error) { requiredDocPaths := requiredDocs(config) var changedFiles []string + var checkErrors []error for _, doc := range requiredDocPaths { - upToDate := NewProcessor(doc, config).IsUpToDate() + upToDate, err := NewProcessor(doc, config).isUpToDate() + if err != nil { + checkErrors = append(checkErrors, err) + continue + } if !upToDate { changedFiles = append(changedFiles, doc) } } - return changedFiles + return changedFiles, checkErrors } func requiredDocs(config configuration.Configuration) []string { diff --git a/fragmentation/cache.go b/fragmentation/cache.go new file mode 100644 index 0000000..b986dc5 --- /dev/null +++ b/fragmentation/cache.go @@ -0,0 +1,115 @@ +// 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 fragmentation + +import ( + "container/list" + "sync" +) + +// cache is a limited collection of recently used values by key. +type cache[K comparable, V any] struct { + sync.Mutex + limit int + loader func(K) (V, error) + values map[K]V + entries map[K]*list.Element + order *list.List +} + +// newCache creates a cache with a loader and least-recently-used eviction. +func newCache[K comparable, V any](limit int, loader func(K) (V, error)) *cache[K, V] { + return &cache[K, V]{ + limit: limit, + loader: loader, + values: make(map[K]V), + entries: make(map[K]*list.Element), + order: list.New(), + } +} + +// get returns a cached value or loads it when missing. +func (c *cache[K, V]) get(key K) (V, error) { + c.Lock() + value, found := c.values[key] + if found { + c.markUsed(key) + c.Unlock() + + return value, nil + } + c.Unlock() + + value, err := c.loader(key) + if err != nil { + return value, err + } + + c.Lock() + defer c.Unlock() + c.storeLoaded(key, value) + + return value, nil +} + +// clear removes all cached values. +func (c *cache[K, V]) clear() { + c.Lock() + defer c.Unlock() + + c.values = make(map[K]V) + c.entries = make(map[K]*list.Element) + c.order.Init() +} + +// storeLoaded stores a loaded value and evicts the least recently used value when needed. +func (c *cache[K, V]) storeLoaded(key K, value V) { + c.values[key] = value + if entry, found := c.entries[key]; found { + c.order.MoveToBack(entry) + return + } + + c.entries[key] = c.order.PushBack(key) + if len(c.values) <= c.limit { + return + } + + c.evictOldest() +} + +// markUsed moves a cache key to the most recently used position. +func (c *cache[K, V]) markUsed(key K) { + if entry, found := c.entries[key]; found { + c.order.MoveToBack(entry) + } +} + +// evictOldest removes the least recently used cache entry. +func (c *cache[K, V]) evictOldest() { + oldestEntry := c.order.Front() + if oldestEntry == nil { + return + } + + oldestKey := oldestEntry.Value.(K) + c.order.Remove(oldestEntry) + delete(c.entries, oldestKey) + delete(c.values, oldestKey) +} diff --git a/fragmentation/fragment.go b/fragmentation/fragment.go index aedfdee..fa45fbd 100644 --- a/fragmentation/fragment.go +++ b/fragmentation/fragment.go @@ -44,20 +44,6 @@ func CreateDefaultFragment() Fragment { } } -// WriteTo takes given lines, unites them into a text and writes it into given file. -// -// file — a FragmentFile to write the lines to. -// -// lines — a list of strings to write. -// -// separator — string to insert between multiple partitions of a single fragment. -// -// Creates the file if not exists and overwrites if exists. -func (f Fragment) WriteTo(file FragmentFile, lines []string, separator string) { - text := f.text(lines, separator) - file.Write(text) -} - func (f Fragment) isDefault() bool { return f.Name == DefaultFragmentName } diff --git a/fragmentation/fragment_file.go b/fragmentation/fragment_file.go deleted file mode 100644 index 9794ca3..0000000 --- a/fragmentation/fragment_file.go +++ /dev/null @@ -1,243 +0,0 @@ -// 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 fragmentation - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "os" - "path" - "path/filepath" - "strings" - - config "embed-code/embed-code-go/configuration" - "embed-code/embed-code-go/files" - _type "embed-code/embed-code-go/type" -) - -// FragmentFile is a file storing a single fragment from the file. -// -// CodePath — a relative path to a code file. The path is relative to the corresponding code root, -// and starts with the code root name if it's provided. -// -// FragmentName — a name of the fragment in the code file. -// -// Configuration — a configuration for embedding. -type FragmentFile struct { - CodePath string - FragmentName string - Configuration config.Configuration -} - -// NewFragmentFileFromAbsolute composes a FragmentFile for the given fragment in given codeFile. -// -// codeFile — an absolute path to a code file. -// -// codeRoot - a _type.NamedPath to the code root. -// -// fragmentName — a name of the fragment in the code file. -// -// configuration — configuration for embedding. -// -// Returns composed fragment. -func NewFragmentFileFromAbsolute( - codeFile string, - codeRoot _type.NamedPath, - fragmentName string, - config config.Configuration, -) FragmentFile { - absolutePath, err := filepath.Abs(codeRoot.Path) - if err != nil { - panic(err) - } - - relativePath, err := filepath.Rel(absolutePath, codeFile) - if err != nil { - panic(err) - } - - if strings.TrimSpace(codeRoot.Name) != "" { - relativePath = path.Join(NamedPathPrefix+codeRoot.Name, filepath.ToSlash(relativePath)) - } else { - relativePath = filepath.ToSlash(relativePath) - } - - return FragmentFile{ - CodePath: relativePath, - FragmentName: fragmentName, - Configuration: config, - } -} - -// Writes the given text to the file. -// -// Creates the file if not exists and overwrites if exists. -func (f FragmentFile) Write(text string) { - byteStr := []byte(text) - filePath := f.absolutePath() - err := os.WriteFile(filePath, byteStr, os.FileMode(files.WritePermission)) - if err != nil { - panic(err) - } -} - -// Content reads content of the file. -// -// Returns contents of the file as a list of strings, or returns an error if it doesn't exist. -func (f FragmentFile) Content() ([]string, error) { - path := f.absolutePath() - isPathFileExits, err := files.IsFileExist(path) - - if err != nil { - return nil, err - } - - if !isPathFileExits { - codeFileReference, err := f.codeFileReference() - if err != nil { - return nil, err - } - - if f.FragmentName != "" { - if f.FragmentName == "_default" { - return nil, fmt.Errorf("code file `%s` not found", codeFileReference) - } - return nil, fmt.Errorf( - "fragment `%s` from code file `%s` not found", f.FragmentName, codeFileReference, - ) - } - return nil, fmt.Errorf( - "code file %s fragment not found", - codeFileReference, - ) - } - - return files.ReadFile(path) -} - -// Returns string representation of FragmentFile. -func (f FragmentFile) String() string { - return f.absolutePath() -} - -// Obtains the absolute path to this fragment file. -func (f FragmentFile) absolutePath() string { - fileExtension := filepath.Ext(f.CodePath) - fragmentsAbsDir, err := filepath.Abs(f.Configuration.FragmentsDir) - if err != nil { - panic(err) - } - - if f.FragmentName == DefaultFragmentName { - return filepath.Join(fragmentsAbsDir, filepath.FromSlash(f.CodePath)) - } - - withoutExtension := filepath.FromSlash(strings.TrimSuffix(f.CodePath, fileExtension)) - filename := fmt.Sprintf("%s-%s", withoutExtension, f.fragmentHash()) - - return filepath.Join(fragmentsAbsDir, filename+fileExtension) -} - -// Builds a user-facing reference to the original code file for error messages. -// -// If the source file exists, returns its absolute `file://` URL. If the file path uses a named -// code root and the resolved file does not exist, returns both the prefixed path and the expanded -// absolute path. -func (f FragmentFile) codeFileReference() (string, error) { - originalCodePath, isPrefixed, err := f.originalCodePath() - if err != nil { - return "", err - } - if originalCodePath == "" { - return f.CodePath, nil - } - - exists, err := files.IsFileExist(originalCodePath) - if err != nil { - return "", err - } - if exists { - return "file://" + originalCodePath, nil - } - if isPrefixed { - return fmt.Sprintf("%s (%s)", f.CodePath, originalCodePath), nil - } - - return originalCodePath, nil -} - -// Resolves the original source file path from the fragment's code path. -// -// Returns the absolute path to the source file and reports whether the input path used a named -// code-root prefix such as `$runtime/...`. -// -// Returns an error if the path uses a named code root that is not present in the configuration. -func (f FragmentFile) originalCodePath() (string, bool, error) { - normalizedPath := filepath.ToSlash(filepath.Clean(f.CodePath)) - - if strings.HasPrefix(normalizedPath, NamedPathPrefix) { - withoutPrefix := strings.TrimPrefix(normalizedPath, NamedPathPrefix) - codeRootName, relativePath, _ := strings.Cut(withoutPrefix, "/") - - for _, codeRoot := range f.Configuration.CodeRoots { - if strings.TrimSpace(codeRoot.Name) != codeRootName { - continue - } - - return filepath.Join(resolvedRootPath(codeRoot.Path), filepath.FromSlash(relativePath)), - true, nil - } - - return "", true, fmt.Errorf("code root with name `%s` not found for path `%s`", - codeRootName, f.CodePath) - } - - if len(f.Configuration.CodeRoots) == 1 { - return filepath.Join( - resolvedRootPath(f.Configuration.CodeRoots[0].Path), - filepath.FromSlash(normalizedPath), - ), false, nil - } - - return "", false, nil -} - -// Resolves the given path to an absolute path when possible. -// -// If absolute-path resolution fails, returns the original path. -func resolvedRootPath(path string) string { - absolutePath, err := filepath.Abs(path) - if err != nil { - return path - } - - return absolutePath -} - -// Calculates and returns a hash string for FragmentFile. -// -// Since fragments which have the same name unite into one fragment with multiple partitions, -// the name of a fragment is unique. -func (f FragmentFile) fragmentHash() string { - hash := sha256.New() - hash.Write([]byte(f.FragmentName)) - - return hex.EncodeToString(hash.Sum(nil))[:8] -} diff --git a/fragmentation/fragmentation.go b/fragmentation/fragmentation.go index b08c666..1e3ebf3 100644 --- a/fragmentation/fragmentation.go +++ b/fragmentation/fragmentation.go @@ -40,21 +40,16 @@ import ( "embed-code/embed-code-go/files" _type "embed-code/embed-code-go/type" "fmt" - "log/slog" "os" "path/filepath" - "strings" config "embed-code/embed-code-go/configuration" - - "github.com/bmatcuk/doublestar/v4" ) // NamedPathPrefix the prefix before the named code source. const NamedPathPrefix = "$" -// Fragmentation splits the given file into fragments and writes them into corresponding -// output files. +// Fragmentation splits the given file into fragments. // // Configuration — a configuration for embedding. // @@ -68,17 +63,6 @@ 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. @@ -151,129 +135,6 @@ func (f Fragmentation) DoFragmentation() ([]string, map[string]Fragment, error) return contentToRender, fragments, nil } -// WriteFragments serializes fragments to the output directory. -// -// Keeps the original directory structure relative to the sources root dir. -// That is, `SRC/src/main` becomes `OUT/src/main`. -// -// 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 nil, err - } - - err = files.EnsureDirExists(f.targetDirectory()) - if err != nil { - return nil, err - } - - for _, fragment := range fragments { - fragmentFile := NewFragmentFileFromAbsolute( - f.CodeFile, f.SourcesRoot, fragment.Name, f.Configuration, - ) - fragment.WriteTo(fragmentFile, allLines, f.Configuration.Separator) - } - - return fragments, nil -} - -// WriteFragmentFiles writes each fragment into a corresponding file. -// -// Searches for code files with patterns defined in configuration and makes fragments of them with -// creating fragmented files as a result. -// -// All fragments are placed inside Configuration.FragmentsDir with keeping the original directory -// structure relative to the sources root dir. -// That is, `SRC/src/main` becomes `OUT/src/main`. -// If code root is named, `SRC/src/main` becomes `OUT/$CODE_ROOT_NAME/src/main` -// -// config — is a configuration for embedding. -// -// Returns an error if any of the fragments couldn't be written. -func WriteFragmentFiles(config config.Configuration) WriteFragmentFilesResult { - includes := config.CodeIncludes - codeRoots := config.CodeRoots - totalSourceFiles := 0 - totalFragments := 0 - for _, codeRoot := range codeRoots { - codeRootFiles := 0 - codeRootFragments := 0 - for _, rule := range includes { - pattern := filepath.Join(codeRoot.Path, rule) - codeFiles, err := doublestar.FilepathGlob(pattern) - codeRootFiles += len(codeFiles) - if err != nil { - panic(err) - } - for _, codeFile := range codeFiles { - fragments, err := writeFragments(config, codeRoot, codeFile) - if err != nil { - panic(err) - } - codeRootFragments += len(fragments) - } - } - totalSourceFiles += codeRootFiles - totalFragments += codeRootFragments - if codeRootFiles > 0 { - slog.Info( - fmt.Sprintf("Found `%d` source code files with `%d` fragments under `%s`%s.", - codeRootFiles, codeRootFragments, codeRoot.Path, configNameLabel(config)), - ) - } else { - slog.Warn( - fmt.Sprintf("No code fragments were found under `%s`%s.", - codeRoot.Path, configNameLabel(config)), - ) - } - } - - return WriteFragmentFilesResult{ - TotalSourceFiles: totalSourceFiles, - TotalFragments: totalFragments, - } -} - -func configNameLabel(config config.Configuration) string { - if config.Name == "" { - return "" - } - return fmt.Sprintf(" for embedding `%s`", config.Name) -} - -// CleanFragmentFiles deletes Configuration.FragmentsDir if it exists. -func CleanFragmentFiles(config config.Configuration) { - exists, err := files.IsDirExist(config.FragmentsDir) - if err != nil { - panic(err) - } - if !exists { - panic(fmt.Errorf("%s directory is not exist", config.FragmentsDir)) - } - if err = os.RemoveAll(config.FragmentsDir); err != nil { - panic(err) - } -} - -// Checks if the code is able to split into fragments and writes them to a file. -func writeFragments( - config config.Configuration, - codeRoot _type.NamedPath, - codeFile string, -) (map[string]Fragment, error) { - if shouldDoFragmentation(codeFile) { - fragmentation := NewFragmentation(codeFile, codeRoot, config) - fragments, err := fragmentation.WriteFragments() - if err != nil { - return nil, err - } - return fragments, nil - } - - return map[string]Fragment{}, nil -} - // shouldDoFragmentation reports whether the file is valid to do fragmentation: // - it exists by the given path // - it is a file (not a dir) @@ -365,25 +226,3 @@ func (f Fragmentation) parseEndDocFragments(endDocFragments []string, cursor int return nil } - -// Obtains the target directory path based on the Configuration.FragmentsDir and the parent -// dir of Fragmentation.CodeFile. -func (f Fragmentation) targetDirectory() string { - fragmentsDir := f.Configuration.FragmentsDir - codeRoot, err := filepath.Abs(f.SourcesRoot.Path) - codeRootName := strings.TrimSpace(f.SourcesRoot.Name) - if err != nil { - panic(fmt.Sprintf("error calculating absolute path: %v", err)) - } - relativeFile, err := filepath.Rel(codeRoot, f.CodeFile) - if err != nil { - panic(fmt.Sprintf("error calculating relative path: %v", err)) - } - subTree := filepath.Dir(relativeFile) - - if codeRootName != "" { - return filepath.Join(fragmentsDir, NamedPathPrefix+codeRootName, subTree) - } - - return filepath.Join(fragmentsDir, subTree) -} diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index e529773..d8bca82 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -20,13 +20,9 @@ package fragmentation_test import ( "embed-code/embed-code-go/configuration" - "embed-code/embed-code-go/files" "embed-code/embed-code-go/fragmentation" _type "embed-code/embed-code-go/type" "fmt" - "os" - "path/filepath" - "regexp" "testing" . "github.com/onsi/ginkgo/v2" @@ -53,112 +49,57 @@ var _ = Describe("Fragmentation", func() { var config configuration.Configuration BeforeEach(func() { + fragmentation.ClearResolverCache() config = configuration.NewConfiguration() config.DocumentationRoot = "../test/resources/docs" config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} }) - AfterEach(func() { - cleanupDir(config.FragmentsDir) - }) - It("should do file fragmentation successfully", func() { - frag := buildTestFragmentation(correctFragmentsFileName, config) - Expect(frag.WriteFragments()).Error().ShouldNot(HaveOccurred()) - - fragmentChild, _ := os.ReadDir(config.FragmentsDir) - Expect(fragmentChild).Should(HaveLen(1)) - Expect(fragmentChild[0].Name()).Should(Equal("org")) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(4)) - - var isDefaultFragmentExist bool - for _, file := range fragmentFiles { - if file.Name() == correctFragmentsFileName { - isDefaultFragmentExist = true - } else { - Expect(file.Name()).Should(MatchRegexp(`Hello-\w+\.java`)) - } - } - - Expect(isDefaultFragmentExist).Should(BeTrue()) - }) - - It("should do multi-source fragmentation successfully", func() { - config := configuration.NewConfiguration() - config.DocumentationRoot = "../test/resources/docs" - javaCodePathName := "java-code" - kotlinCodePathName := "kotlin-code" - config.CodeRoots = _type.NamedPathList{ - _type.NamedPath{ - Name: javaCodePathName, - Path: "../test/resources/code/java/org/example/multitest", - }, - _type.NamedPath{ - Name: kotlinCodePathName, - Path: "../test/resources/code/kotlin/org/example/multitest", - }, - } - result := fragmentation.WriteFragmentFiles(config) - Expect(result.TotalSourceFiles).Should(Equal(2)) - javaFragments, _ := os.ReadDir( - filepath.Join(config.FragmentsDir, fragmentation.NamedPathPrefix+javaCodePathName), - ) - kotlinFragments, _ := os.ReadDir( - filepath.Join(config.FragmentsDir, fragmentation.NamedPathPrefix+kotlinCodePathName), - ) - Expect(javaFragments).Should(HaveLen(2)) - Expect(kotlinFragments).Should(HaveLen(2)) + lines, fragments := doTestFragmentation(correctFragmentsFileName, config) + + Expect(lines).ShouldNot(ContainElement(ContainSubstring("#docfragment"))) + Expect(lines).ShouldNot(ContainElement(ContainSubstring("#enddocfragment"))) + Expect(fragments).Should(HaveKey(fragmentation.DefaultFragmentName)) + Expect(fragments).Should(HaveKey("Without License")) + Expect(fragments).Should(HaveKey("Hello class")) + Expect(fragments).Should(HaveKey("main()")) }) - It("should do fragmentation of a fragment without end", func() { - frag := buildTestFragmentation(unclosedFragmentFileName, config) - Expect(frag.WriteFragments()).Error().ShouldNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(2)) - - fragmentFileName := findFragmentFile(fragmentFiles, unclosedFragmentFileName) - fragmentsDir := fragmentsDirPath(config.FragmentsDir) - content, err := os.ReadFile(filepath.Join(fragmentsDir, fragmentFileName)) - if err != nil { - Fail(err.Error()) - } - - re := regexp.MustCompile(`[.\n\s]+}\n}\n`) - matchedStrings := re.FindStringSubmatch(string(content)) + It("should resolve named fragments", func() { + content := resolveTestFragment(correctFragmentsFileName, "main()", config) - Expect(matchedStrings).Should(Not(BeEmpty())) + Expect(content).Should(Equal([]string{ + "public static void main(String[] args) {", + indent + "System.out.println(\"Hello world\");", + "}", + })) }) - It("should not do fragmentation of an empty file", func() { - frag := buildTestFragmentation(emptyFileName, config) - Expect(frag.WriteFragments()).Error().ShouldNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(1)) - fragmentsFilePath := fragmentsDirPath(config.FragmentsDir) + "/" + fragmentFiles[0].Name() - - content, err := os.ReadFile(fragmentsFilePath) - if err != nil { - Fail(err.Error()) - } + It("should resolve fragments without an end marker through the end of the file", func() { + content := resolveTestFragment(unclosedFragmentFileName, "Fragment that never ends", config) - Expect(content).Should(BeEmpty()) + Expect(content).Should(Equal([]string{ + indent + indent + "System.out.println(\"Hello world\");", + indent + "}", + "}", + })) }) - It("should not do fragmentation of a binary file", func() { - config.CodeIncludes = []string{"**.jar"} + It("should fragment an empty file", func() { + lines, fragments := doTestFragmentation(emptyFileName, config) - Expect(fragmentation.WriteFragmentFiles(config).TotalFragments).Should(Equal(0)) - Expect(files.IsDirExist(config.FragmentsDir)).Should(BeFalse()) + Expect(lines).Should(BeEmpty()) + Expect(fragments).Should(HaveLen(1)) + Expect(fragments).Should(HaveKey(fragmentation.DefaultFragmentName)) }) - It("should not do fragmentation of an unopened fragment", func() { + It("should fail on an unopened fragment", func() { frag := buildTestFragmentation(unopenedFragmentFileName, config) - Expect(frag.WriteFragments()).Error().Should(HaveOccurred()) + _, _, err := frag.DoFragmentation() + + Expect(err).Should(HaveOccurred()) }) Context("fragments parsing", func() { @@ -203,20 +144,7 @@ var _ = Describe("Fragmentation", func() { }) It("should correctly parse file into many partitions", func() { - frag := buildTestFragmentation(complexFragmentsFileName, config) - _, err := frag.WriteFragments() - Expect(err).ToNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(2)) - - fragmentFileName := findFragmentFile(fragmentFiles, complexFragmentsFileName) - fragmentDir := fragmentsDirPath(config.FragmentsDir) - - content, err := files.ReadFile(fmt.Sprintf("%s/%s", fragmentDir, fragmentFileName)) - if err != nil { - Fail(err.Error()) - } + content := resolveTestFragment(complexFragmentsFileName, "Main", config) expected := []string{ "public class Main {", @@ -233,17 +161,10 @@ var _ = Describe("Fragmentation", func() { }) It("should correctly parse file with several different fragments", func() { - frag := buildTestFragmentation(twoFragmentsFileName, config) - _, err := frag.WriteFragments() - Expect(err).ToNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(3)) - - fragmentDir := fragmentsDirPath(config.FragmentsDir) - actual := readFragmentsContent(fragmentDir, fragmentFiles, twoFragmentsFileName) + mainContent := resolveTestFragment(twoFragmentsFileName, "Main", config) + helloContent := resolveTestFragment(twoFragmentsFileName, "Hello", config) - expected := [][]string{ + Expect([][]string{mainContent, helloContent}).Should(ConsistOf([][]string{ { "public class TwoFragments {", indent + config.Separator, @@ -262,23 +183,14 @@ var _ = Describe("Fragmentation", func() { indent + "System.out.println(coolText);", "}", }, - } - - Expect(actual).Should(ConsistOf(expected)) + })) }) It("should correctly parse file with several overlapping fragments", func() { - frag := buildTestFragmentation(overlappingFragmentsFileName, config) - _, err := frag.WriteFragments() - Expect(err).ToNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(3)) + mainContent := resolveTestFragment(overlappingFragmentsFileName, "Main", config) + helloContent := resolveTestFragment(overlappingFragmentsFileName, "Hello", config) - fragmentDir := fragmentsDirPath(config.FragmentsDir) - actual := readFragmentsContent(fragmentDir, fragmentFiles, overlappingFragmentsFileName) - - expected := [][]string{ + Expect([][]string{mainContent, helloContent}).Should(ConsistOf([][]string{ { "public class OverlappingFragments {", indent + config.Separator, @@ -301,9 +213,7 @@ var _ = Describe("Fragmentation", func() { config.Separator, "}", }, - } - - Expect(actual).Should(ConsistOf(expected)) + })) }) }) @@ -315,60 +225,29 @@ func buildTestFragmentation(testFileName string, return fragmentation.NewFragmentation(testFilePath, codeRoot, config) } -func readFragmentsDir(config configuration.Configuration) []os.DirEntry { - fragmentFiles, err := os.ReadDir(fragmentsDirPath(config.FragmentsDir)) - if err != nil { - Fail(err.Error()) - } +func doTestFragmentation( + testFileName string, + config configuration.Configuration, +) ([]string, map[string]fragmentation.Fragment) { + frag := buildTestFragmentation(testFileName, config) - return fragmentFiles -} - -// readFragmentsContent reads the contents of fragment files from a given directory. -// -// fragmentDir — path to the directory containing fragment files. -// fragmentFiles — list of directory entries representing the fragment files. -// skipFile — file name to skip (the default fragment file). -// -// Returns a slice of string slices, where each inner slice contains the lines -// of a fragment file. -// -// This function fails the test immediately if any file cannot be read. -func readFragmentsContent( - fragmentDir string, fragmentFiles []os.DirEntry, skipFile string, -) [][]string { - var result [][]string - for _, file := range fragmentFiles { - if file.Name() == skipFile { - continue - } - - content, err := files.ReadFile(fmt.Sprintf("%s/%s", fragmentDir, file.Name())) - Expect(err).ShouldNot(HaveOccurred()) - - result = append(result, content) - } - - return result -} - -func fragmentsDirPath(path string) string { - return fmt.Sprintf("%s/org/example", path) -} - -func findFragmentFile(files []os.DirEntry, fileName string) string { - for _, file := range files { - if file.Name() != fileName { - return file.Name() - } - } + lines, fragments, err := frag.DoFragmentation() - return "" + Expect(err).ShouldNot(HaveOccurred()) + return lines, fragments } -func cleanupDir(dirPath string) { - err := os.RemoveAll(dirPath) - if err != nil { - Fail(err.Error()) - } +func resolveTestFragment( + testFileName string, + fragmentName string, + config configuration.Configuration, +) []string { + content, err := fragmentation.ResolveContent( + fmt.Sprintf("org/example/%s", testFileName), + fragmentName, + config, + ) + + Expect(err).ShouldNot(HaveOccurred()) + return content } diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go new file mode 100644 index 0000000..84f26ec --- /dev/null +++ b/fragmentation/resolver.go @@ -0,0 +1,205 @@ +// 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 fragmentation + +import ( + "fmt" + "path/filepath" + "strings" + + config "embed-code/embed-code-go/configuration" + _type "embed-code/embed-code-go/type" +) + +// resolverCacheLimit is the maximum number of source files retained in the resolver cache. +const resolverCacheLimit = 100 + +// fragmentedFile stores cleaned source lines and parsed fragments for one source file. +type fragmentedFile struct { + lines []string + fragments map[string]Fragment +} + +// resolverCache stores source fragmentations already resolved during the current run. +var resolverCache = newCache[resolvedPath, fragmentedFile]( + resolverCacheLimit, + loadSourceFragments, +) + +// ResolveContent returns source lines for the requested code file fragment. +// +// Named fragments are extracted directly from the source file on demand and cached by source file. +func ResolveContent(codePath string, fragmentName string, config config.Configuration) ([]string, error) { + if fragmentName == "" { + fragmentName = DefaultFragmentName + } + + source, found, err := resolveSource(codePath, config) + if err != nil { + return nil, err + } + if !found { + return nil, unresolvedSourceError(codePath, fragmentName, config) + } + + content, err := cachedSourceFragments(source) + if err != nil { + return nil, err + } + + fragment, found := content.fragments[fragmentName] + if !found { + codeFileReference := "file://" + source.absolutePath + return nil, fmt.Errorf("fragment `%s` from code file `%s` not found", + fragmentName, codeFileReference) + } + + return fragmentLines(fragment, content.lines, config.Separator), nil +} + +// ClearResolverCache removes cached source fragmentations. +func ClearResolverCache() { + resolverCache.clear() +} + +// resolvedPath is a source file path resolved from a user-facing embedding path. +type resolvedPath struct { + root _type.NamedPath + relativePath string + absolutePath string +} + +// resolveSource resolves the user-facing code path to the source file. +func resolveSource(codePath string, config config.Configuration) (resolvedPath, bool, error) { + codeRootName, relativePath, named := splitNamedPath(codePath) + for _, root := range config.CodeRoots { + if named && strings.TrimSpace(root.Name) != codeRootName { + continue + } + + source, err := sourceFromRoot(root, relativePath) + if err != nil { + return resolvedPath{}, false, err + } + if !shouldDoFragmentation(source.absolutePath) { + continue + } + + return source, true, nil + } + + return resolvedPath{}, false, nil +} + +// splitNamedPath separates a named-code-root prefix from a code path. +func splitNamedPath(codePath string) (string, string, bool) { + normalizedPath := filepath.ToSlash(filepath.Clean(codePath)) + if !strings.HasPrefix(normalizedPath, NamedPathPrefix) { + return "", normalizedPath, false + } + + withoutPrefix := strings.TrimPrefix(normalizedPath, NamedPathPrefix) + rootName, relativePath, _ := strings.Cut(withoutPrefix, "/") + + return rootName, relativePath, true +} + +// sourceFromRoot builds a source path from a code root and a relative path. +func sourceFromRoot(root _type.NamedPath, relativePath string) (resolvedPath, error) { + rootAbs, err := filepath.Abs(root.Path) + if err != nil { + return resolvedPath{}, err + } + + return resolvedPath{ + root: root, + relativePath: filepath.FromSlash(relativePath), + absolutePath: filepath.Join(rootAbs, filepath.FromSlash(relativePath)), + }, nil +} + +// cachedSourceFragments returns cached source fragmentation for a resolved source file. +func cachedSourceFragments(source resolvedPath) (fragmentedFile, error) { + return resolverCache.get(source) +} + +// loadSourceFragments reads and fragments the source file when it is not already cached. +func loadSourceFragments(source resolvedPath) (fragmentedFile, error) { + fragmentation := NewFragmentation(source.absolutePath, source.root, config.Configuration{}) + lines, fragments, err := fragmentation.DoFragmentation() + if err != nil { + return fragmentedFile{}, err + } + return fragmentedFile{ + lines: lines, + fragments: fragments, + }, nil +} + +// fragmentLines renders a fragment into lines. +func fragmentLines(fragment Fragment, lines []string, separator string) []string { + text := fragment.text(lines, separator) + if text == "" { + return []string{} + } + + return strings.Split(strings.TrimSuffix(text, "\n"), "\n") +} + +// unresolvedSourceError builds an error for a code path that cannot be resolved from sources. +func unresolvedSourceError(codePath string, fragmentName string, config config.Configuration) error { + codeFileReference, err := codeFileReference(codePath, config) + if err != nil { + return err + } + if fragmentName == DefaultFragmentName { + return fmt.Errorf("code file `%s` not found", codeFileReference) + } + + return fmt.Errorf("fragment `%s` from code file `%s` not found", + fragmentName, codeFileReference) +} + +// codeFileReference builds a user-facing source reference for unresolved code paths. +func codeFileReference(codePath string, config config.Configuration) (string, error) { + codeRootName, relativePath, named := splitNamedPath(codePath) + for _, root := range config.CodeRoots { + if named && strings.TrimSpace(root.Name) != codeRootName { + continue + } + + source, err := sourceFromRoot(root, relativePath) + if err != nil { + return "", err + } + if named { + return fmt.Sprintf("%s (%s)", codePath, source.absolutePath), nil + } + if len(config.CodeRoots) == 1 { + return source.absolutePath, nil + } + } + + if named { + return "", fmt.Errorf("code root with name `%s` not found for path `%s`", + codeRootName, codePath) + } + + return codePath, nil +} diff --git a/main.go b/main.go index f40913a..98c7615 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,7 @@ import ( ) // Version of the embed-code application. -const Version = "1.1.1" +const Version = "1.2.0" // The entry point for embed-code. // @@ -69,9 +69,6 @@ const Version = "1.1.1" // then the checking for up-to-date is performed. If it is set to 'embed', the embedding // is performed. // If it is set to 'analyze', the analyzing is performed; -// - code-includes — a comma-separated string of glob patterns for code files to include. -// For example: -// "**/*.java,**/*.gradle". Default value is "**/*.*"; // - doc-includes — a comma-separated string of glob patterns for docs files to include. // For example: // "docs/**/*.md,guides/*.html". Default value is "**/*.md,**/*.html"; @@ -79,8 +76,6 @@ const Version = "1.1.1" // the embedding. // For example: // "old-docs/**/*.md,old-guides/*.html". It is not set by default; -// - fragments-path — a path to a directory with code fragments. Default value is -// "./build/fragments"; // - separator — a string which is used as a separator between code fragments. Default value // is "...". func main() { @@ -113,10 +108,7 @@ func main() { switch userArgs.Mode { case cli.ModeCheck: - for _, config := range configs { - cli.CheckCodeSamples(config) - } - fmt.Println("The documentation files are up-to-date with code files.") + checkByConfigs(configs) case cli.ModeEmbed: embedByConfigs(configs) fmt.Println("Embedding process finished.") @@ -142,30 +134,46 @@ func logError(message string, err error) { slog.Error(fmt.Sprintf("%s: %v", message, err)) } +// checkByConfigs runs check for all configs and logs outdated documentation files. +func checkByConfigs(configs []configuration.Configuration) { + var totalOutdatedFiles []string + for _, config := range configs { + totalOutdatedFiles = append(totalOutdatedFiles, cli.CheckCodeSamples(config)...) + } + if len(totalOutdatedFiles) == 0 { + fmt.Println("The documentation files are up-to-date with code files.") + + return + } + + printFiles("File outdated:", "Files outdated:", totalOutdatedFiles) +} + // embedByConfig runs the embedByConfig for all configs and logs the results. func embedByConfigs(configs []configuration.Configuration) { var totalEmbeddedFiles []string totalEmbeddings := 0 - totalFragments := 0 for _, config := range configs { result := cli.EmbedCodeSamples(config) totalEmbeddedFiles = append(totalEmbeddedFiles, result.UpdatedTargetFiles...) totalEmbeddings += result.TotalEmbeddings - totalFragments += result.TotalFragments } - if len(totalEmbeddedFiles) == 0 && - totalEmbeddings != 0 && - totalFragments != 0 { + if len(totalEmbeddedFiles) == 0 && totalEmbeddings != 0 { fmt.Println("All documentation files are already up to date. Nothing to update.") } - if len(totalEmbeddedFiles) == 1 { - fmt.Println("File updated:") + printFiles("File updated:", "Files updated:", totalEmbeddedFiles) +} + +// printFiles prints file paths with the singular or plural heading. +func printFiles(singularHeading string, pluralHeading string, files []string) { + if len(files) == 1 { + fmt.Println(singularHeading) } - if len(totalEmbeddedFiles) > 1 { - fmt.Println("Files updated:") + if len(files) > 1 { + fmt.Println(pluralHeading) } - for _, updatedDocFile := range totalEmbeddedFiles { - absPath, err := filepath.Abs(updatedDocFile) + for _, file := range files { + absPath, err := filepath.Abs(file) if err != nil { panic(err) } diff --git a/test/resources/code/java/plain-text-to-embed.txt b/test/resources/code/java/plain-text-to-embed.txt new file mode 100644 index 0000000..dc2bec0 --- /dev/null +++ b/test/resources/code/java/plain-text-to-embed.txt @@ -0,0 +1,7 @@ +This line contains foo in the middle +This line ends with foo +foo — this line starts with it + +This line contains bar in the middle +bar — this line starts with it +This line ends with bar