Skip to content

Commit 6d55f07

Browse files
committed
Initial commit
0 parents  commit 6d55f07

File tree

6 files changed

+273
-0
lines changed

6 files changed

+273
-0
lines changed

.github/workflows/release.yml

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
jobs:
9+
build:
10+
runs-on: macos-latest
11+
steps:
12+
- uses: actions/checkout@v3
13+
14+
- name: Set up Go
15+
uses: actions/setup-go@v4
16+
with:
17+
go-version: '>=1.19.0'
18+
19+
- name: Build
20+
run: go build -v -o md2cb
21+
22+
- name: Create Release
23+
id: create_release
24+
uses: actions/create-release@v1
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
with:
28+
tag_name: ${{ github.ref }}
29+
release_name: Release ${{ github.ref }}
30+
draft: false
31+
prerelease: false
32+
33+
- name: Upload Release Asset
34+
uses: actions/upload-release-asset@v1
35+
env:
36+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37+
with:
38+
upload_url: ${{ steps.create_release.outputs.upload_url }}
39+
asset_path: ./md2cb
40+
asset_name: md2cb
41+
asset_content_type: application/octet-stream

LICENSE.txt

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Hans Raaf
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# md2cb - Markdown to Clipboard with Rich Text support
2+
3+
A simple command line tool that converts markdown to rich text and copies it to the clipboard. Perfect for converting markdown tables to rich text that can be pasted into applications that support formatted text.
4+
5+
## Requirements
6+
7+
- macOS (uses native Cocoa framework for clipboard operations)
8+
- Go 1.17 or later
9+
- pandoc (`brew install pandoc`)
10+
11+
## Installation
12+
13+
```bash
14+
go install github.com/oderwat/md2cb@latest
15+
```
16+
17+
Or download the latest release from the [releases page](https://github.com/oderwat/md2cb/releases).
18+
19+
## Usage
20+
21+
The program reads markdown from stdin and places the converted rich text on the clipboard:
22+
23+
```bash
24+
echo "| Header | Content |
25+
|-|-|
26+
| Cell 1 | Cell 2 |" | md2cb
27+
```
28+
29+
Or read from a file:
30+
31+
```bash
32+
cat your_table.md | md2cb
33+
```
34+
35+
## How it Works
36+
37+
1. Uses pandoc to convert markdown to HTML
38+
2. Converts the HTML to rich text using macOS's native NSAttributedString
39+
3. Places the result on the clipboard in both HTML and rich text formats
40+
4. Ready to paste into any application that supports formatted text
41+
42+
## License
43+
44+
MIT License - see [LICENSE.txt](LICENSE.txt) file

go.mod

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module github.com/oderwat/md2cb
2+
3+
go 1.23.4
4+
5+
require golang.design/x/clipboard v0.7.0
6+
7+
require (
8+
github.com/progrium/darwinkit v0.5.0 // indirect
9+
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
10+
golang.org/x/image v0.6.0 // indirect
11+
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect
12+
golang.org/x/sys v0.5.0 // indirect
13+
)

go.sum

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
2+
github.com/progrium/darwinkit v0.5.0 h1:SwchcMbTOG1py3CQsINmGlsRmYKdlFrbnv3dE4aXA0s=
3+
github.com/progrium/darwinkit v0.5.0/go.mod h1:PxQhZuftnALLkCVaR8LaHtUOfoo4pm8qUDG+3C/sXNs=
4+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
5+
golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo=
6+
golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E=
7+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
8+
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
9+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
10+
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
11+
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
12+
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
13+
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
14+
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
15+
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
16+
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
17+
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
18+
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
19+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
20+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
21+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
22+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
23+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
24+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
25+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
26+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
27+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
28+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
29+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
30+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
31+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
32+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
33+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
35+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
36+
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
37+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
38+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
39+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
40+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
41+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
42+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
43+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
44+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
45+
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
46+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
47+
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
48+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
49+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
50+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
51+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

main.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package main
2+
3+
/*
4+
#cgo CFLAGS: -x objective-c
5+
#cgo LDFLAGS: -framework Foundation -framework AppKit
6+
#import <Foundation/Foundation.h>
7+
#import <AppKit/AppKit.h>
8+
9+
void copyToClipboard(const char* html) {
10+
@autoreleasepool {
11+
NSString *htmlString = [NSString stringWithUTF8String:html];
12+
NSData *htmlData = [htmlString dataUsingEncoding:NSUTF8StringEncoding];
13+
14+
NSDictionary *options = @{
15+
NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
16+
NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)
17+
};
18+
19+
NSAttributedString *attributedString = [[NSAttributedString alloc]
20+
initWithData:htmlData
21+
options:options
22+
documentAttributes:nil
23+
error:nil];
24+
25+
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
26+
[pasteboard clearContents];
27+
[pasteboard writeObjects:@[attributedString]];
28+
}
29+
}
30+
*/
31+
import "C"
32+
import (
33+
"bytes"
34+
"fmt"
35+
"io"
36+
"os"
37+
"os/exec"
38+
"unsafe"
39+
)
40+
41+
func convertMarkdownToHTML(markdown string) (string, error) {
42+
cmd := exec.Command("pandoc", "-f", "markdown", "-t", "html")
43+
cmd.Stdin = bytes.NewBufferString(markdown)
44+
45+
var out bytes.Buffer
46+
var stderr bytes.Buffer
47+
cmd.Stdout = &out
48+
cmd.Stderr = &stderr
49+
50+
if err := cmd.Run(); err != nil {
51+
return "", fmt.Errorf("error running pandoc: %v - %s", err, stderr.String())
52+
}
53+
54+
return out.String(), nil
55+
}
56+
57+
func copyToClipboardGo(html string) error {
58+
// Format as complete HTML document
59+
doc := fmt.Sprintf(`<!DOCTYPE html>
60+
<html>
61+
<head>
62+
<meta charset="utf-8">
63+
<style>
64+
table { border-collapse: collapse; }
65+
th, td { border: 1px solid black; padding: 8px; }
66+
</style>
67+
</head>
68+
<body>
69+
%s
70+
</body>
71+
</html>`, html)
72+
73+
cstr := C.CString(doc)
74+
defer C.free(unsafe.Pointer(cstr))
75+
C.copyToClipboard(cstr)
76+
return nil
77+
}
78+
79+
func main() {
80+
input, err := io.ReadAll(os.Stdin)
81+
if err != nil {
82+
fmt.Fprintf(os.Stderr, "Error reading from stdin: %v\n", err)
83+
os.Exit(1)
84+
}
85+
86+
if len(input) == 0 {
87+
fmt.Fprintf(os.Stderr, "No input received on stdin\n")
88+
os.Exit(1)
89+
}
90+
91+
html, err := convertMarkdownToHTML(string(input))
92+
if err != nil {
93+
fmt.Fprintf(os.Stderr, "Error converting markdown: %v\n", err)
94+
os.Exit(1)
95+
}
96+
97+
if err := copyToClipboardGo(html); err != nil {
98+
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
99+
os.Exit(1)
100+
}
101+
102+
fmt.Fprintf(os.Stderr, "Content copied to clipboard successfully!\n")
103+
}

0 commit comments

Comments
 (0)