Skip to content

Instantly share code, notes, and snippets.

@lmika
Created January 9, 2025 06:15
Show Gist options
  • Save lmika/796c31a7a06d1fa53f14541aee09a79c to your computer and use it in GitHub Desktop.
Save lmika/796c31a7a06d1fa53f14541aee09a79c to your computer and use it in GitHub Desktop.
Obsidian Kanban board to HTML
package main
import (
"bufio"
"bytes"
"fmt"
"os"
"regexp"
"strings"
"unicode"
"github.com/gomarkdown/markdown"
)
func main() {
// Read the Markdown input from stdin
input := readInput()
// Convert the Markdown to HTML
html := parseKanbanToHTML(input)
// Output the resulting HTML
fmt.Println(html)
}
func readInput() string {
var buffer bytes.Buffer
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
buffer.WriteString(scanner.Text() + "\n")
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
os.Exit(1)
}
return buffer.String()
}
func parseKanbanToHTML(markdownInput string) string {
lines := strings.Split(markdownInput, "\n")
// Remove front matter if present
lines = stripFrontMatter(lines)
columnRegex := regexp.MustCompile(`^##\s+(.*)`)
trailerRegex := regexp.MustCompile(`^%%`)
cardRegex := regexp.MustCompile(`^- \[[ x]\](.*)`)
html := &strings.Builder{}
html.WriteString("<html><head><style>body{font-family:sans-serif;} .column{margin:10px;padding:10px;border:1px solid #ddd;display:inline-block;vertical-align:top;width:30%;} .card{margin:5px;padding:10px;border:1px solid #ccc;background:#f9f9f9;} .card-title{font-weight:bold;margin-bottom:5px;} .card-body{margin-top:5px;}</style></head><body>")
var currentColumn string
var cardBuffer strings.Builder
inCard := false
for _, line := range lines {
line = strings.TrimRightFunc(line, unicode.IsSpace)
if trailerRegex.MatchString(line) {
break
}
if columnRegex.MatchString(line) {
// Close the previous column div, if any
if inCard {
html.WriteString(fmt.Sprintf("<div class=\"card-body\">%s</div></div>", markdown.ToHTML([]byte(trimTabsLeftUpto(cardBuffer.String())), nil, nil)))
}
if currentColumn != "" {
html.WriteString("</div>")
}
// Start a new column div
matches := columnRegex.FindStringSubmatch(line)
columnName := matches[1]
currentColumn = columnName
inCard = false
cardBuffer.Reset()
html.WriteString(fmt.Sprintf("<div class=\"column\"><h2>%s</h2>", columnName))
} else if cardRegex.MatchString(line) {
// If we are in a card, flush its contents
if inCard {
html.WriteString(fmt.Sprintf("<div class=\"card-body\">%s</div></div>", markdown.ToHTML([]byte(trimTabsLeftUpto(cardBuffer.String())), nil, nil)))
cardBuffer.Reset()
}
// Start a new card
matches := cardRegex.FindStringSubmatch(line)
cardTitle := strings.TrimSpace(matches[1])
html.WriteString(fmt.Sprintf("<div class=\"card\"><div class=\"card-title\">%s</div>", cardTitle))
inCard = true
} else if inCard {
// Accumulate the card body content
if len(line) > 0 {
cardBuffer.WriteString(line + "\n")
}
}
}
// Close the last card and column divs if necessary
if inCard {
html.WriteString(fmt.Sprintf("<div class=\"card-body\">%s</div></div>", markdown.ToHTML([]byte(trimTabsLeftUpto(cardBuffer.String())), nil, nil)))
}
if currentColumn != "" {
html.WriteString("</div>")
}
html.WriteString("</body></html>")
return html.String()
}
func stripFrontMatter(lines []string) []string {
if len(lines) > 0 && strings.TrimSpace(lines[0]) == "---" {
for i := 1; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) == "---" {
return lines[i+1:]
}
}
}
return lines
}
func trimTabsLeftUpto(lines string) string {
res := strings.Builder{}
leftMargin := 0
for i, s := range strings.Split(lines, "\n") {
if i == 0 {
// Find the left margin
for i, c := range s {
if !unicode.IsSpace(c) {
leftMargin = i
break
}
}
} else {
res.WriteString("\n")
}
if leftMargin == 0 || len(s) < leftMargin {
res.WriteString(s)
} else {
res.WriteString(s[leftMargin:])
}
}
return res.String()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment