Created
January 9, 2025 06:15
-
-
Save lmika/796c31a7a06d1fa53f14541aee09a79c to your computer and use it in GitHub Desktop.
Obsidian Kanban board to HTML
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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