Skip to content

Commit 3d9486c

Browse files
committed
feat: add --insert flag
1 parent da18a70 commit 3d9486c

12 files changed

Lines changed: 344 additions & 19 deletions

File tree

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Table of Contents
3939
* [Remote files](#remote-files)
4040
* [Multiple files](#multiple-files)
4141
* [Combo](#combo)
42+
* [Auto insert and update TOC](#auto-insert-and-update-toc)
4243
* [Starting Depth](#starting-depth)
4344
* [Depth](#depth)
4445
* [No Escape](#no-escape)
@@ -91,6 +92,8 @@ Flags:
9192
--token=TOKEN GitHub personal token
9293
--indent=2 Indent space of generated list
9394
--debug Show debug info
95+
--insert Insert TOC into file (auto-insert at top or between <!--ts--> and <!--te--> markers)
96+
--no-backup Skip creating backup file when using --insert
9497
--version Show application version.
9598

9699
Args:
@@ -298,6 +301,45 @@ You can easily combine both ways:
298301
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
299302
```
300303
304+
Auto insert and update TOC
305+
----------
306+
307+
You can easily insert a TOC into an existing Markdown file. Just add the following placeholder in your document:
308+
309+
```markdown
310+
<!--ts-->
311+
<!--te-->
312+
```
313+
314+
Now run the tool:
315+
316+
```bash
317+
$ ./gh-md-toc --insert README.md
318+
319+
Table of Contents
320+
=================
321+
322+
* [gh-md-toc](#gh-md-toc)
323+
* [Installation](#installation)
324+
* [Usage](#usage)
325+
326+
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)
327+
```
328+
329+
The TOC will be automatically inserted between the `<!--ts-->` and `<!--te-->` markers.
330+
331+
If your file doesn't have these markers, the TOC will be auto-inserted at the top (before the first heading).
332+
333+
Next time when your file will be changed just repeat the command (`./gh-md-toc --insert ...`) and TOC will be refreshed again.
334+
335+
A backup of your original file will be created with the `.YYYY-MM-DD_HHMMSS` suffix.
336+
337+
If you don't want to create a backup, use `--no-backup` option:
338+
339+
```bash
340+
$ ./gh-md-toc --insert --no-backup README.md
341+
```
342+
301343
Starting Depth
302344
--------------
303345

cmd/gh-md-toc/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ var (
2424
debug = kingpin.Flag("debug", "Show debug info").Bool()
2525
ghurl = kingpin.Flag("github-url", "GitHub URL, default=https://api.github.com").Default("https://api.github.com").String()
2626
reVersion = kingpin.Flag("re-version", "RegExp version, default=0").Default(version.GH_2024_03).String()
27+
insert = kingpin.Flag("insert", "Insert TOC into file (auto-insert at top or between <!--ts--> and <!--te--> markers)").Bool()
28+
noBackup = kingpin.Flag("no-backup", "Skip creating backup file when using --insert").Bool()
2729
)
2830

2931
// Entry point
@@ -52,6 +54,8 @@ func main() {
5254
GHToken: *token,
5355
GHUrl: *ghurl,
5456
GHVersion: *reVersion,
57+
Insert: *insert,
58+
NoBackup: *noBackup,
5559
}
5660

5761
if err := app.New(cfg).Run(os.Stdout); err != nil {

internal/adapters/filebackup.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package adapters
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"time"
8+
)
9+
10+
type FileBackupper struct{}
11+
12+
func NewFileBackupper() *FileBackupper {
13+
return &FileBackupper{}
14+
}
15+
16+
func (fb *FileBackupper) CreateBackup(filepath string) (string, error) {
17+
timestamp := time.Now().Format("2006-01-02_150405")
18+
backupPath := fmt.Sprintf("%s.%s", filepath, timestamp)
19+
20+
src, err := os.Open(filepath)
21+
if err != nil {
22+
return "", err
23+
}
24+
defer func() {
25+
_ = src.Close()
26+
}()
27+
28+
dst, err := os.Create(backupPath)
29+
if err != nil {
30+
return "", err
31+
}
32+
defer func() {
33+
_ = dst.Close()
34+
}()
35+
36+
if _, err := io.Copy(dst, src); err != nil {
37+
return "", err
38+
}
39+
40+
return backupPath, nil
41+
}

internal/adapters/tocinserter.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package adapters
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"os/user"
8+
"strings"
9+
"time"
10+
)
11+
12+
const (
13+
MarkerStart = "<!--ts-->"
14+
MarkerEnd = "<!--te-->"
15+
)
16+
17+
type TocInserter struct {
18+
hideFooter bool
19+
}
20+
21+
func NewTocInserter(hideFooter bool) *TocInserter {
22+
return &TocInserter{hideFooter: hideFooter}
23+
}
24+
25+
func (ti *TocInserter) InsertToc(filepath string, toc string) error {
26+
stat, err := os.Stat(filepath)
27+
if err != nil {
28+
return err
29+
}
30+
31+
content, err := os.ReadFile(filepath)
32+
if err != nil {
33+
return err
34+
}
35+
36+
contentStr := string(content)
37+
38+
if err := ti.validateMarkers(contentStr); err != nil {
39+
return fmt.Errorf("invalid markers in %s: %w", filepath, err)
40+
}
41+
42+
var newContent string
43+
if ti.hasMarkers(contentStr) {
44+
newContent, err = ti.replaceContent(contentStr, toc)
45+
if err != nil {
46+
return err
47+
}
48+
} else {
49+
newContent = ti.insertAtTop(contentStr, toc)
50+
}
51+
52+
return os.WriteFile(filepath, []byte(newContent), stat.Mode().Perm())
53+
}
54+
55+
func (ti *TocInserter) hasMarkers(content string) bool {
56+
return strings.Contains(content, MarkerStart) && strings.Contains(content, MarkerEnd)
57+
}
58+
59+
func (ti *TocInserter) validateMarkers(content string) error {
60+
startCount := strings.Count(content, MarkerStart)
61+
endCount := strings.Count(content, MarkerEnd)
62+
63+
if startCount != endCount {
64+
return fmt.Errorf("mismatched markers: found %d start markers and %d end markers", startCount, endCount)
65+
}
66+
if startCount > 1 {
67+
return fmt.Errorf("multiple marker pairs found (%d pairs), only one pair is supported", startCount)
68+
}
69+
if startCount == 0 && endCount == 0 {
70+
return nil
71+
}
72+
return nil
73+
}
74+
75+
func (ti *TocInserter) replaceContent(content, toc string) (string, error) {
76+
lines := strings.Split(content, "\n")
77+
var result []string
78+
var insideMarkers bool
79+
var markerFound bool
80+
81+
for _, line := range lines {
82+
trimmed := strings.TrimSpace(line)
83+
84+
if trimmed == MarkerStart {
85+
result = append(result, line)
86+
insideMarkers = true
87+
markerFound = true
88+
result = append(result, ti.formatToc(toc))
89+
continue
90+
}
91+
92+
if trimmed == MarkerEnd {
93+
result = append(result, line)
94+
insideMarkers = false
95+
continue
96+
}
97+
98+
if !insideMarkers {
99+
result = append(result, line)
100+
}
101+
}
102+
103+
if !markerFound {
104+
return "", fmt.Errorf("markers not found")
105+
}
106+
107+
return strings.Join(result, "\n"), nil
108+
}
109+
110+
func (ti *TocInserter) formatToc(toc string) string {
111+
var buf bytes.Buffer
112+
113+
buf.WriteString("\n")
114+
buf.WriteString(toc)
115+
116+
if !ti.hideFooter {
117+
buf.WriteString("\n")
118+
buf.WriteString(ti.generateTimestamp())
119+
buf.WriteString("\n")
120+
}
121+
122+
return buf.String()
123+
}
124+
125+
func (ti *TocInserter) generateTimestamp() string {
126+
username := "unknown"
127+
if u, err := user.Current(); err == nil {
128+
username = u.Username
129+
}
130+
131+
timestamp := time.Now().Format("2006-01-02T15:04-07:00")
132+
return fmt.Sprintf("<!-- Added by: %s, at: %s -->", username, timestamp)
133+
}
134+
135+
func (ti *TocInserter) insertAtTop(content, toc string) string {
136+
lines := strings.Split(content, "\n")
137+
var result []string
138+
var insertIndex int
139+
var foundHeading bool
140+
141+
for i, line := range lines {
142+
trimmed := strings.TrimSpace(line)
143+
if strings.HasPrefix(trimmed, "#") {
144+
insertIndex = i
145+
foundHeading = true
146+
break
147+
}
148+
}
149+
150+
if !foundHeading {
151+
insertIndex = 0
152+
}
153+
154+
result = append(result, lines[:insertIndex]...)
155+
result = append(result, MarkerStart)
156+
result = append(result, ti.formatToc(toc))
157+
result = append(result, MarkerEnd)
158+
result = append(result, "")
159+
result = append(result, lines[insertIndex:]...)
160+
161+
return strings.Join(result, "\n")
162+
}

internal/app/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type Config struct {
1919
GHToken string
2020
GHUrl string
2121
GHVersion string
22+
Insert bool
23+
NoBackup bool
2224
}
2325

2426
func (c Config) ToControllerConfig() controller.Config {
@@ -35,6 +37,8 @@ func (c Config) ToControllerConfig() controller.Config {
3537
GHToken: c.GHToken,
3638
GHUrl: c.GHUrl,
3739
GHVersion: c.GHVersion,
40+
Insert: c.Insert,
41+
NoBackup: c.NoBackup,
3842
}
3943
}
4044

internal/app/new.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ func New(cfg Config) *App {
3232
grabberJson := adapters.NewJsonGrabber(cfg.ToGrabberConfig())
3333
getter := adapters.NewRemoteGetter(true)
3434
temper := adapters.NewFileTemper()
35+
fileBackupper := adapters.NewFileBackupper()
36+
tocInserter := adapters.NewTocInserter(cfg.HideFooter)
3537

3638
log.Info("App.New: init usecases ...")
3739
ucLocalMD, ucRemoteMD, ucRemoteHTML := usecase.New(
@@ -40,7 +42,7 @@ func New(cfg Config) *App {
4042
)
4143

4244
log.Info("App.New: init controller ...")
43-
ctl := controller.New(ctlCfg, ucLocalMD, ucRemoteMD, ucRemoteHTML, log)
45+
ctl := controller.New(ctlCfg, ucLocalMD, ucRemoteMD, ucRemoteHTML, log, fileBackupper, tocInserter)
4446

4547
log.Info("App.New: done.")
4648
return &App{

internal/controller/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ type Config struct {
1717
GHToken string
1818
GHUrl string
1919
GHVersion string
20+
Insert bool
21+
NoBackup bool
2022
}
2123

2224
func (c Config) ToUseCaseConfig() config.Config {
@@ -33,5 +35,7 @@ func (c Config) ToUseCaseConfig() config.Config {
3335
GHUrl: c.GHUrl,
3436
GHVersion: c.GHVersion,
3537
AbsPathInToc: len(c.Files) > 1,
38+
Insert: c.Insert,
39+
NoBackup: c.NoBackup,
3640
}
3741
}

0 commit comments

Comments
 (0)