Skip to content

Commit 85cc4b3

Browse files
authored
Merge pull request #2 from neongreen/copilot/fix-bb410bf8-6466-4605-a8ad-925fdd32e456
Add GitHub Actions workflow, default in-place formatting, --check flag, abbreviation handling, and integration documentation for markdown-format
2 parents 605fbeb + b662a46 commit 85cc4b3

9 files changed

Lines changed: 307 additions & 29 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: markdown-format
2+
3+
on:
4+
push:
5+
paths:
6+
- 'markdown-format/**'
7+
pull_request:
8+
paths:
9+
- 'markdown-format/**'
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version: '1.24.7'
23+
24+
- name: Run tests
25+
working-directory: markdown-format
26+
run: go test -v

markdown-format/README.md

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,51 @@ Formatting markdown with one sentence per line makes it easier to:
2323

2424
## Installation
2525

26+
### With Go
27+
2628
```bash
2729
go build
2830
```
2931

32+
### With mise
33+
34+
Install using [mise](https://mise.jdx.dev/) with the Go backend:
35+
36+
```bash
37+
mise use -g go:github.com/neongreen/mono/markdown-format@main
38+
```
39+
40+
Or add to your `.mise.toml`:
41+
42+
```toml
43+
[tools]
44+
"go:github.com/neongreen/mono/markdown-format" = "main"
45+
```
46+
3047
## Usage
3148

32-
Format a markdown file:
49+
Format files in place (default behavior):
3350

3451
```bash
35-
./markdown-format input.md > output.md
52+
./markdown-format file1.md file2.md file3.md
3653
```
3754

38-
Read from stdin:
55+
Check if files are formatted without modifying them:
3956

4057
```bash
41-
cat input.md | ./markdown-format - > output.md
58+
./markdown-format --check file1.md file2.md
59+
```
60+
61+
Format from stdin to stdout:
62+
63+
```bash
64+
cat input.md | ./markdown-format
65+
```
66+
67+
Or:
68+
69+
```bash
70+
./markdown-format < input.md > output.md
4271
```
4372

4473
## Example
@@ -59,6 +88,62 @@ It has multiple sentences.
5988
Let's format it!
6089
```
6190

91+
## Integration with Formatting Tools
92+
93+
See the [examples/](examples/) directory for complete configuration files and sample markdown files demonstrating the integrations.
94+
95+
### treefmt
96+
97+
[treefmt](https://github.com/numtide/treefmt) is a universal code formatter that runs multiple formatters with one command.
98+
99+
Add to your `treefmt.toml`:
100+
101+
```toml
102+
[formatter.markdown-format]
103+
command = "markdown-format"
104+
includes = ["*.md"]
105+
```
106+
107+
Then run:
108+
109+
```bash
110+
treefmt
111+
```
112+
113+
### dprint
114+
115+
[dprint](https://dprint.dev/) is a pluggable and configurable code formatter.
116+
117+
To integrate markdown-format with dprint, use the exec plugin. First, install the exec plugin if you haven't already:
118+
119+
```bash
120+
dprint config add exec
121+
```
122+
123+
Then add to your `dprint.json`:
124+
125+
```json
126+
{
127+
"exec": {
128+
"commands": [{
129+
"command": "markdown-format",
130+
"exts": ["md"]
131+
}]
132+
},
133+
"plugins": [
134+
"https://plugins.dprint.dev/exec-0.5.0.json@<hash>"
135+
]
136+
}
137+
```
138+
139+
The exec plugin will handle passing files to markdown-format for in-place formatting.
140+
141+
Run dprint:
142+
143+
```bash
144+
dprint fmt
145+
```
146+
62147
## License
63148

64149
MIT License - See LICENSE file in the repository root.

markdown-format/examples/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Integration Examples
2+
3+
This directory contains example configurations and files for integrating markdown-format with various formatting tools.
4+
5+
## Files
6+
7+
- `treefmt.toml` - Example configuration for treefmt
8+
- `dprint.json` - Example configuration for dprint
9+
- `sample-input.md` - Sample markdown file before formatting
10+
- `sample-output.md` - Sample markdown file after formatting with markdown-format
11+
12+
## Testing the Integration
13+
14+
### With treefmt
15+
16+
1. Install treefmt (https://github.com/numtide/treefmt)
17+
2. Copy `treefmt.toml` to your project root
18+
3. Ensure `markdown-format` is in your PATH or update the `command` in `treefmt.toml`
19+
4. Run `treefmt` to format all markdown files
20+
21+
### With dprint
22+
23+
1. Install dprint (https://dprint.dev/)
24+
2. Add the exec plugin: `dprint config add exec`
25+
3. Copy `dprint.json` to your project root (or merge the configuration)
26+
4. Ensure `markdown-format` is in your PATH or update the `command` in the exec configuration
27+
5. Run `dprint fmt` to format all markdown files
28+
29+
## Verifying the Integration
30+
31+
You can test the integration by:
32+
33+
1. Creating a copy of `sample-input.md` in your project
34+
2. Running your formatter (treefmt or dprint)
35+
3. Comparing the result with `sample-output.md`
36+
37+
The output should match, with one sentence per line while preserving all markdown structure.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "https://dprint.dev/schemas/v0.json",
3+
"exec": {
4+
"commands": [{
5+
"command": "markdown-format",
6+
"exts": ["md"]
7+
}]
8+
},
9+
"plugins": [
10+
"https://plugins.dprint.dev/exec-0.5.0.json@<hash>"
11+
],
12+
"excludes": [
13+
"**/*-lock.json",
14+
"**/node_modules",
15+
"**/.git"
16+
]
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Sample Document
2+
3+
This is a paragraph with multiple sentences. It should be formatted with one sentence per line. This makes git diffs easier to read.
4+
5+
## Features
6+
7+
- First item with a long description. This has multiple sentences.
8+
- Second item with nested content:
9+
- Nested item one
10+
- Nested item two with more details
11+
- Third item with more text. And even more!
12+
- Items can use abbreviations, e.g. this one. They should be handled correctly.
13+
14+
## Code Example
15+
16+
```python
17+
def hello():
18+
print("Hello, world!")
19+
```
20+
21+
Conclusion paragraph. Final thoughts here. That's all!
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Sample Document
2+
3+
This is a paragraph with multiple sentences.
4+
It should be formatted with one sentence per line.
5+
This makes git diffs easier to read.
6+
7+
## Features
8+
9+
- First item with a long description.
10+
This has multiple sentences.
11+
- Second item with nested content:- Nested item one
12+
- Nested item two with more details
13+
- Third item with more text.
14+
And even more!
15+
- Items can use abbreviations, e.g. this one.
16+
They should be handled correctly.
17+
18+
## Code Example
19+
20+
```python
21+
def hello():
22+
print("Hello, world!")
23+
```
24+
25+
Conclusion paragraph.
26+
Final thoughts here.
27+
That's all!
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Example treefmt configuration for markdown-format
2+
# Place this file in your project root as treefmt.toml
3+
4+
[formatter.markdown-format]
5+
command = "markdown-format"
6+
options = []
7+
includes = ["*.md"]
8+
excludes = []

markdown-format/main.go

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import (
1414
"github.com/yuin/goldmark/text"
1515
)
1616

17-
// sentenceBoundaryRegex matches sentence boundaries (. ! ?) followed by space or end of string
18-
var sentenceBoundaryRegex = regexp.MustCompile(`([.!?])(\s+)`)
17+
// Common abbreviations that should not be treated as sentence boundaries
18+
var commonAbbreviations = []string{"e.g.", "i.e.", "etc.", "vs.", "cf.", "ex.", "viz.", "approx.", "ca."}
19+
20+
// sentenceBoundaryRegex matches sentence boundaries (. ! ?) followed by space and uppercase letter
21+
var sentenceBoundaryRegex = regexp.MustCompile(`([.!?])(\s+)([A-Z])`)
1922

2023
// formatMarkdown formats markdown content with one sentence per line
2124
func formatMarkdown(input []byte) ([]byte, error) {
@@ -294,16 +297,29 @@ func collectInlineText(node ast.Node, source []byte, buf *bytes.Buffer) error {
294297

295298
// splitIntoSentences splits text into sentences
296299
func splitIntoSentences(text string) []string {
300+
// Protect common abbreviations by replacing them temporarily
301+
protected := text
302+
replacements := make(map[string]string)
303+
for i, abbr := range commonAbbreviations {
304+
placeholder := fmt.Sprintf("\x00ABBR%d\x00", i)
305+
replacements[placeholder] = abbr
306+
protected = strings.ReplaceAll(protected, abbr, placeholder)
307+
}
308+
297309
// Replace sentence boundaries with a special marker
298-
text = sentenceBoundaryRegex.ReplaceAllString(text, "$1\n\n")
310+
protected = sentenceBoundaryRegex.ReplaceAllString(protected, "$1\n\n$3")
299311

300312
// Split by the marker
301-
parts := strings.Split(text, "\n\n")
313+
parts := strings.Split(protected, "\n\n")
302314

303315
var sentences []string
304316
for _, part := range parts {
305317
part = strings.TrimSpace(part)
306318
if part != "" {
319+
// Restore abbreviations
320+
for placeholder, abbr := range replacements {
321+
part = strings.ReplaceAll(part, placeholder, abbr)
322+
}
307323
sentences = append(sentences, part)
308324
}
309325
}
@@ -312,39 +328,58 @@ func splitIntoSentences(text string) []string {
312328
}
313329

314330
func main() {
331+
checkFlag := flag.Bool("check", false, "check if files are formatted without modifying them")
315332
flag.Parse()
316333

317334
args := flag.Args()
335+
336+
// If no arguments, read from stdin and write to stdout
318337
if len(args) == 0 {
319-
fmt.Fprintln(os.Stderr, "Usage: markdown-format <file>")
320-
fmt.Fprintln(os.Stderr, " or pipe input via stdin")
321-
os.Exit(1)
322-
}
323-
324-
var input []byte
325-
var err error
326-
327-
if args[0] == "-" {
328-
// Read from stdin
329-
input, err = io.ReadAll(os.Stdin)
338+
input, err := io.ReadAll(os.Stdin)
330339
if err != nil {
331340
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
332341
os.Exit(1)
333342
}
334-
} else {
335-
// Read from file
336-
input, err = os.ReadFile(args[0])
343+
output, err := formatMarkdown(input)
337344
if err != nil {
338-
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
345+
fmt.Fprintf(os.Stderr, "Error formatting markdown: %v\n", err)
339346
os.Exit(1)
340347
}
348+
fmt.Print(string(output))
349+
return
341350
}
342351

343-
output, err := formatMarkdown(input)
344-
if err != nil {
345-
fmt.Fprintf(os.Stderr, "Error formatting markdown: %v\n", err)
352+
// Handle file(s) - in-place by default
353+
hasErrors := false
354+
for _, filename := range args {
355+
input, err := os.ReadFile(filename)
356+
if err != nil {
357+
fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", filename, err)
358+
os.Exit(1)
359+
}
360+
output, err := formatMarkdown(input)
361+
if err != nil {
362+
fmt.Fprintf(os.Stderr, "Error formatting %s: %v\n", filename, err)
363+
os.Exit(1)
364+
}
365+
366+
if *checkFlag {
367+
// Check mode: compare without modifying
368+
if !bytes.Equal(input, output) {
369+
fmt.Fprintf(os.Stderr, "%s: not formatted\n", filename)
370+
hasErrors = true
371+
}
372+
} else {
373+
// Default: write in-place
374+
err = os.WriteFile(filename, output, 0644)
375+
if err != nil {
376+
fmt.Fprintf(os.Stderr, "Error writing file %s: %v\n", filename, err)
377+
os.Exit(1)
378+
}
379+
}
380+
}
381+
382+
if hasErrors {
346383
os.Exit(1)
347384
}
348-
349-
fmt.Print(string(output))
350385
}

0 commit comments

Comments
 (0)