diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..00f314d --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,23 @@ +name: golangci-lint +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f576800 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Go +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.25.0' + - name: Install dependencies + run: go get . + - name: Build + run: go build -v ./... + - name: Test with the Go CLI + run: go test \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..64bb863 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,32 @@ +version: "2" + +linters: + enable: + - bodyclose + - errcheck + - goconst + - gocritic + - govet + - ineffassign + - mirror + - misspell + - nolintlint + - paralleltest + - revive + - staticcheck + - unconvert + - unparam + - unused + - whitespace + +formatters: + enable: + - gci + - gofmt + - goimports + settings: + gci: + sections: + - standard + - default + - localmodule \ No newline at end of file diff --git a/README.md b/README.md index 8d70794..e40988f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # codemd -Parse source code files and generate a markdown file + +Convert between markdown files containing code blocks and individual source files. + +## Installation + +```bash +go install github.com/lynxai-team/codemd@latest +``` + +## Usage + +### Convert source files to markdown + +```bash +codemd tomd -f main.go -f helper.js -o docs.md +``` + +Or scan a directory: + +```bash +codemd tomd -d ./src -o docs.md +``` diff --git a/codemd.go b/codemd.go new file mode 100644 index 0000000..b2f9b17 --- /dev/null +++ b/codemd.go @@ -0,0 +1,319 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +// Document represents a collection of code blocks that can be converted +// between markdown and source files. +type Document struct { + Blocks []CodeBlock +} + +// CodeBlock represents a single code block with its metadata. +type CodeBlock struct { + Filename string + Language string + Content string +} + +// NewDocument returns an empty Document. +func NewDocument() *Document { + return &Document{ + Blocks: []CodeBlock{}, + } +} + +// Reset clears all blocks from the document. +func (d *Document) Reset() { + d.Blocks = []CodeBlock{} +} + +// ParseMarkdown reads a markdown file and adds extracted code blocks to the document. +func (d *Document) ParseMarkdown(markdownPath string) error { + data, err := os.ReadFile(markdownPath) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + + lines := strings.Split(string(data), "\n") + + var inCodeBlock bool + var language string + var content strings.Builder + var lastHeaderFilename string + fileCounter := 0 + + for _, line := range lines { + if inCodeBlock { + if strings.HasPrefix(strings.TrimSpace(line), "```") { + fileCounter++ + + d.Blocks = append(d.Blocks, CodeBlock{ + Filename: filename(lastHeaderFilename, language, fileCounter), + Language: language, + Content: content.String(), + }) + + inCodeBlock = false + language = "" + content.Reset() + lastHeaderFilename = "" + continue + } + content.WriteString(line + "\n") + continue + } + + switch line := strings.TrimSpace(line); { + case strings.HasPrefix(line, "```"): + inCodeBlock = true + language = strings.TrimSpace(line[3:]) + case strings.HasPrefix(line, "## "): + headerText := strings.TrimSpace(line[3:]) + if headerText != "" { + lastHeaderFilename = headerText + } + case line == "": + continue + default: + lastHeaderFilename = "" + } + } + + return nil +} + +// filename determines the appropriate filename for a code block. +// It prefers the header filename if its extension matches the language, +// otherwise generates a filename based on the language and counter. +func filename(headerFilename, language string, counter int) string { + defaultName := fmt.Sprintf("file%d", counter) + + ext := determineFileExtension(language) + if ext == "" { + return defaultName + } + + if headerFilename != "" && filepath.Ext(headerFilename) == ext { + return headerFilename + } + + return fmt.Sprintf("file%d%s", counter, ext) +} + +// ParseFiles reads source files and adds them as blocks to the document. +func (d *Document) ParseFiles(filePaths ...string) error { + for _, filePath := range filePaths { + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return fmt.Errorf("file does not exist: %s", filePath) + } + + filename := filepath.Base(filePath) + language := detectSourceLanguage(filename) + if language == "" { + continue + } + + content, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + block := CodeBlock{ + Filename: filePath, + Language: language, + Content: string(content), + } + + d.Blocks = append(d.Blocks, block) + } + + return nil +} + +// ParseDir recursively scans a directory for source files and adds them to the document. +func (d *Document) ParseDir(dir string) error { + files, err := getFilesFromDirectory(dir) + if err != nil { + return fmt.Errorf("failed to scan directory: %w", err) + } + + sort.Strings(files) + + return d.ParseFiles(files...) +} + +// ToMarkdown writes the Document as a markdown file. +func (d *Document) ToMarkdown(outputPath string) error { + file, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer func() { _ = file.Close() }() + + if _, err := file.WriteString("# Code Files\n\n"); err != nil { + return fmt.Errorf("failed to write header: %w", err) + } + + for i, block := range d.Blocks { + if i > 0 { + if _, err := file.WriteString("\n"); err != nil { + return fmt.Errorf("failed to write spacing: %w", err) + } + } + + if _, err := fmt.Fprintf(file, "## %s\n\n", block.Filename); err != nil { + return fmt.Errorf("failed to write filename header: %w", err) + } + + if _, err := fmt.Fprintf(file, "```%s\n", block.Language); err != nil { + return fmt.Errorf("failed to write code block start: %w", err) + } + + if _, err := fmt.Fprintf(file, "%s```\n", block.Content); err != nil { + return fmt.Errorf("failed to write code block: %w", err) + } + } + + return nil +} + +// ToSourceFiles writes the Document as individual source files. +func (d *Document) ToSourceFiles(outputDir string) error { + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + for _, block := range d.Blocks { + fullPath := filepath.Join(outputDir, block.Filename) + + dir := filepath.Dir(fullPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + + if err := os.WriteFile(fullPath, []byte(block.Content), 0644); err != nil { + return fmt.Errorf("failed to write file %s: %w", fullPath, err) + } + } + + return nil +} + +// defaultSkipPatterns contains patterns for paths to skip during directory scanning +var defaultSkipPatterns = []string{ + ".git/", + ".gitignore", + ".gitattributes", + ".svn/", + ".hg/", + + "node_modules/", + "vendor/", + ".venv/", + "venv/", + "env/", + "__pycache__/", + + "dist/", + "build/", + "target/", + "bin/", + "obj/", + ".next/", + "out/", + "coverage/", + + ".vscode/", + ".idea/", + "*.swp", + "*.swo", + ".DS_Store", + "Thumbs.db", + + ".pytest_cache/", + ".tox/", + "*.egg-info/", + ".mypy_cache/", + ".coverage", + + ".env", + ".env.*", + ".npmrc", + + "package-lock.json", + "yarn.lock", + "Cargo.lock", + "pnpm-lock.yaml", + "poetry.lock", + "go.sum", + + "*.exe", + "*.dll", + "*.so", + "*.dylib", + "*.o", + "*.obj", + "*.a", + "*.lib", + + "*.log", + "*.tmp", + "*.temp", + + ".*", +} + +// getFilesFromDirectory returns all source files in a directory recursively, +// skipping common non-source directories and files. +func getFilesFromDirectory(dir string) ([]string, error) { + var files []string + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if shouldSkip(path, info.IsDir(), defaultSkipPatterns) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + + if !info.IsDir() { + files = append(files, path) + } + return nil + }) + return files, err +} + +// shouldSkip checks if a path matches any skip pattern +func shouldSkip(path string, isDir bool, patterns []string) bool { + name := filepath.Base(path) + + for _, pattern := range patterns { + if strings.HasSuffix(pattern, "/") { + if !isDir { + continue + } + dirPattern := strings.TrimSuffix(pattern, "/") + if matched, _ := filepath.Match(dirPattern, name); matched { + return true + } + } else { + if matched, _ := filepath.Match(pattern, name); matched { + if pattern == ".*" && name == "." { + continue + } + return true + } + } + } + return false +} diff --git a/codemd_test.go b/codemd_test.go new file mode 100644 index 0000000..bb2209a --- /dev/null +++ b/codemd_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMarkdownToSourceFiles(t *testing.T) { + t.Parallel() + + doc := NewDocument() + tempDir := t.TempDir() + + require.NoError(t, doc.ParseMarkdown("testdata/golden.md")) + require.NoError(t, doc.ToSourceFiles(tempDir)) + + filePairs := []struct { + actual string + wanted string + }{ + {"main.c", "testdata/code/main.c"}, + {"helper.js", "testdata/code/helper.js"}, + {"file3.css", "testdata/code/main.css"}, + } + + for _, cf := range filePairs { + assertEqualFile( + t, + filepath.Join(tempDir, cf.actual), + cf.wanted, + ) + } +} + +func TestSourceFilesToMarkdown(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + parseFunc func(doc *Document) error + }{ + "individual files": { + parseFunc: func(doc *Document) error { + return doc.ParseFiles( + "testdata/code/helper.js", + "testdata/code/main.c", + "testdata/code/main.css", + ) + }, + }, + "directory": { + parseFunc: func(doc *Document) error { + return doc.ParseDir("testdata/code") + }, + }, + } + + for name, cc := range cases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + doc := NewDocument() + + require.NoError(t, cc.parseFunc(doc)) + + outputPath := filepath.Join(t.TempDir(), "generated.md") + + require.NoError(t, doc.ToMarkdown(outputPath)) + assertEqualFile(t, outputPath, "testdata/golden-code.md") + }) + } +} + +func assertEqualFile(t *testing.T, actualPath, expectedPath string) { + t.Helper() + + actualContent, err := os.ReadFile(actualPath) + require.NoError(t, err, "read file %s", actualPath) + + expectedContent, err := os.ReadFile(expectedPath) + require.NoError(t, err, "read file %s", expectedPath) + + assert.Equal(t, string(expectedContent), string(actualContent)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1af0198 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/lynxai-team/codemd + +go 1.25.0 + +require ( + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/spf13/pflag v1.0.9 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4074d86 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d2c024c --- /dev/null +++ b/main.go @@ -0,0 +1,139 @@ +// codemd-lite is a lightweight tool to convert between markdown files +// containing code blocks and individual source code files. +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +func main() { + cmd := CommandMdCodeLite() + + if err := cmd.Execute(); err != nil { + cmd.PrintErr(err) + os.Exit(1) + } +} + +func CommandMdCodeLite() *cobra.Command { + var cmd = &cobra.Command{ + Use: "codemd", + Short: "A tool to convert between markdown and source files", + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, + } + + cmd.AddCommand(CommandToCode()) + cmd.AddCommand(CommandToMarkdown()) + + return cmd +} + +func CommandToCode() *cobra.Command { + var mdFile string + var outputDir string + + cmd := &cobra.Command{ + Use: "tocode", + Short: "Convert markdown file to source files", + Example: `codemd tocode -i docs.md -o src`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + if mdFile == "" { + return fmt.Errorf("markdown file is required (use --input or -i)") + } + if outputDir == "" { + return fmt.Errorf("output directory is required (use --output or -o)") + } + + doc := NewDocument() + + cmd.Printf("Parsing markdown file: %s\n", mdFile) + if err := doc.ParseMarkdown(mdFile); err != nil { + return fmt.Errorf("failed to parse markdown: %w", err) + } + + cmd.Printf("Extracting %d code blocks to: %s\n", len(doc.Blocks), outputDir) + if err := doc.ToSourceFiles(outputDir); err != nil { + return fmt.Errorf("failed to extract files: %w", err) + } + + cmd.Printf("Successfully extracted:") + for _, block := range doc.Blocks { + cmd.Printf(" - %s (%s)\n", block.Filename, block.Language) + } + + return nil + }, + } + + cmd.Flags().StringVarP(&mdFile, "input", "i", "", "Input markdown file to extract from") + _ = cmd.MarkFlagRequired("input") + + cmd.Flags().StringVarP(&outputDir, "output", "o", "", "Output directory for extracted files") + _ = cmd.MarkFlagRequired("output") + + return cmd +} + +func CommandToMarkdown() *cobra.Command { + var outputFile string + var directory string + var files []string + + cmd := &cobra.Command{ + Use: "tomd", + Short: "Convert source files to markdown", + Example: `codemd tomd -f file1 -f file2 -o output.md`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + if outputFile == "" { + return fmt.Errorf("output file is required (use --output or -o)") + } + + doc := NewDocument() + + if directory == "" && len(files) == 0 { + return fmt.Errorf("must specify either --dir or --files (or both)") + } + + if directory != "" { + cmd.Printf("Scanning directory: %s\n", directory) + if err := doc.ParseDir(directory); err != nil { + return fmt.Errorf("failed to scan directory: %w", err) + } + } + + if len(files) > 0 { + cmd.Printf("Parsing %d files\n", len(files)) + if err := doc.ParseFiles(files...); err != nil { + return fmt.Errorf("failed to parse files: %w", err) + } + } + + cmd.Printf("Generating markdown with %d code blocks: %s\n", len(doc.Blocks), outputFile) + if err := doc.ToMarkdown(outputFile); err != nil { + return fmt.Errorf("failed to generate markdown: %w", err) + } + + cmd.Printf("Successfully generated markdown with:") + for _, block := range doc.Blocks { + cmd.Printf(" - %s (%s)\n", block.Filename, block.Language) + } + + return nil + }, + } + + cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output markdown file") + _ = cmd.MarkFlagRequired("output") + + cmd.Flags().StringVarP(&directory, "dir", "d", "", "Directory to scan for source files") + cmd.Flags().StringSliceVarP(&files, "files", "f", nil, "Files to include") + + return cmd +} diff --git a/markdown.go b/markdown.go new file mode 100644 index 0000000..1e3c518 --- /dev/null +++ b/markdown.go @@ -0,0 +1,175 @@ +package main + +import ( + "path/filepath" + "strings" +) + +var extToLanguage = map[string]string{ + // Programming languages + ".go": "go", + ".js": "javascript", + ".ts": "typescript", + ".py": "python", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".cxx": "cpp", + ".cs": "csharp", + ".php": "php", + ".rb": "ruby", + ".rs": "rust", + ".swift": "swift", + ".kt": "kotlin", + ".scala": "scala", + ".pl": "perl", + ".lua": "lua", + ".r": "r", + + // Web technologies + ".html": "html", + ".htm": "html", + ".css": "css", + ".xml": "xml", + ".json": "json", + ".yml": "yaml", + ".yaml": "yaml", + ".toml": "toml", + + // Shell/scripting + ".sh": "bash", + ".ps1": "powershell", + ".bat": "batch", + ".cmd": "batch", + ".fish": "fish", + ".zsh": "zsh", + + // Database + ".sql": "sql", + + // Markup/config + ".md": "markdown", + ".tex": "latex", + ".ini": "ini", + ".properties": "properties", + ".dockerfile": "dockerfile", + ".makefile": "makefile", + ".gitignore": "gitignore", + + // Data formats + ".csv": "csv", + ".jsonl": "jsonl", + ".tsv": "tsv", + + // Other formats + ".txt": "text", + ".diff": "diff", + ".log": "log", + ".conf": "conf", +} + +var languageToExt = map[string]string{ + // Programming languages + "go": ".go", + "javascript": ".js", + "js": ".js", + "typescript": ".ts", + "ts": ".ts", + "python": ".py", + "py": ".py", + "java": ".java", + "c": ".c", + "cpp": ".cpp", + "c++": ".cpp", + "cxx": ".cpp", + "csharp": ".cs", + "c#": ".cs", + "cs": ".cs", + "php": ".php", + "ruby": ".rb", + "rb": ".rb", + "rust": ".rs", + "rs": ".rs", + "swift": ".swift", + "kotlin": ".kt", + "kt": ".kt", + "scala": ".scala", + "perl": ".pl", + "lua": ".lua", + "r": ".r", + + // Web technologies + "html": ".html", + "css": ".css", + "xml": ".xml", + "json": ".json", + "yaml": ".yml", + "yml": ".yml", + "toml": ".toml", + + // Shell/scripting + "bash": ".sh", + "shell": ".sh", + "sh": ".sh", + "powershell": ".ps1", + "ps1": ".ps1", + "batch": ".bat", + "cmd": ".bat", + "bat": ".bat", + "fish": ".fish", + "zsh": ".zsh", + + // Database + "sql": ".sql", + "mysql": ".sql", + "postgresql": ".sql", + "postgres": ".sql", + "sqlite": ".sql", + + // Markup/config + "markdown": ".md", + "md": ".md", + "latex": ".tex", + "tex": ".tex", + "ini": ".ini", + "properties": ".properties", + "dockerfile": ".dockerfile", + "docker": ".dockerfile", + "makefile": ".makefile", + "make": ".makefile", + "gitignore": ".gitignore", + + // Data formats + "csv": ".csv", + "jsonl": ".jsonl", + "tsv": ".tsv", + + // Other formats + "text": ".txt", + "txt": ".txt", + "plain": ".txt", + "diff": ".diff", + "log": ".log", + "conf": ".conf", + "config": ".conf", +} + +// detectSourceLanguage returns the programming language based on the filename extension. +func detectSourceLanguage(filename string) string { + ext := strings.ToLower(filepath.Ext(filename)) + if language, ok := extToLanguage[ext]; ok { + return language + } + + return "" +} + +// determineFileExtension returns the appropriate file extension for a given programming language. +func determineFileExtension(language string) string { + key := strings.ToLower(language) + if ext, ok := languageToExt[key]; ok { + return ext + } + + return "" +} diff --git a/testdata/code/.gitignore b/testdata/code/.gitignore new file mode 100644 index 0000000..8e8874f --- /dev/null +++ b/testdata/code/.gitignore @@ -0,0 +1 @@ +ignored-file.txt diff --git a/testdata/code/helper.js b/testdata/code/helper.js new file mode 100644 index 0000000..4ed55b5 --- /dev/null +++ b/testdata/code/helper.js @@ -0,0 +1,5 @@ +function add(a, b) { + return a + b; +} + +module.exports = { add }; diff --git a/testdata/code/main.c b/testdata/code/main.c new file mode 100644 index 0000000..0923337 --- /dev/null +++ b/testdata/code/main.c @@ -0,0 +1,6 @@ +#include + +int main() { + printf("Hello, World!\n"); + return 0; +} diff --git a/testdata/code/main.css b/testdata/code/main.css new file mode 100644 index 0000000..ee014f1 --- /dev/null +++ b/testdata/code/main.css @@ -0,0 +1,9 @@ +body { + margin: 0; + font-family: Arial, sans-serif; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} diff --git a/testdata/golden-code.md b/testdata/golden-code.md new file mode 100644 index 0000000..b7a0aff --- /dev/null +++ b/testdata/golden-code.md @@ -0,0 +1,36 @@ +# Code Files + +## testdata/code/helper.js + +```javascript +function add(a, b) { + return a + b; +} + +module.exports = { add }; +``` + +## testdata/code/main.c + +```c +#include + +int main() { + printf("Hello, World!\n"); + return 0; +} +``` + +## testdata/code/main.css + +```css +body { + margin: 0; + font-family: Arial, sans-serif; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} +``` diff --git a/testdata/golden.md b/testdata/golden.md new file mode 100644 index 0000000..25144ef --- /dev/null +++ b/testdata/golden.md @@ -0,0 +1,36 @@ +# Simple Test + +This is a test markdown file for md-code-lite. + +## main.c + +```c +#include + +int main() { + printf("Hello, World!\n"); + return 0; +} +``` + +## helper.js + +```javascript +function add(a, b) { + return a + b; +} + +module.exports = { add }; +``` + +```css +body { + margin: 0; + font-family: Arial, sans-serif; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} +```