Skip to content

Instantly share code, notes, and snippets.

@d0rc
Created September 12, 2024 14:40
Show Gist options
  • Save d0rc/90330f4659ce0ad9f7badc7060ad2d92 to your computer and use it in GitHub Desktop.
Save d0rc/90330f4659ce0ad9f7badc7060ad2d92 to your computer and use it in GitHub Desktop.
package main
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/russross/blackfriday/v2"
)
type Section struct {
ID string
Title string
Content template.HTML
SubSections []Section
}
type PageData struct {
Title string
Sections []Section
}
const templateHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-okaidia.min.css">
<style>
/* CSS styles from the previous template */
body, html {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
height: 100%;
line-height: 1.6;
}
.container {
display: flex;
height: 100%;
}
.column {
height: 100%;
overflow-y: auto;
}
.left-column {
width: 230px;
background-color: #2E3336;
color: #fff;
position: fixed;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
}
.middle-column {
width: calc(100% - 230px - 40%);
background-color: #F3F7F9;
border-right: 1px solid #f0f4f7;
margin-left: 230px;
}
.right-column {
width: 40%;
background-color: #2E3336;
color: #fff;
position: fixed;
top: 0;
right: 0;
height: 100%;
overflow-y: auto;
}
.content-wrapper {
padding: 28px;
}
h1, h2, h3, h4 {
margin-top: 2em;
margin-bottom: 0.8em;
}
h1 {
font-size: 25px;
padding-top: 0.5em;
padding-bottom: 0.5em;
margin-bottom: 21px;
margin-top: 2em;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
h2 {
font-size: 19px;
}
h3 {
font-size: 15px;
}
a {
color: #fff;
text-decoration: none;
}
.left-column ul {
list-style-type: none;
padding-left: 20px;
}
.left-column li {
margin-bottom: 10px;
transition: all 0.3s ease;
}
.left-column li.active > a {
color: #007bff;
}
pre[class*="language-"] {
background-color: #1E2224;
margin: 0;
border-radius: 5px;
}
code[class*="language-"] {
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.5;
}
.language-switcher {
display: flex;
justify-content: flex-start;
margin-bottom: 20px;
}
.language-button {
padding: 5px 10px;
background-color: #1E2224;
border: none;
color: #fff;
cursor: pointer;
margin-right: 10px;
}
.language-button.active {
background-color: #007bff;
}
.code-block {
display: none;
}
.code-block.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="column left-column">
<div class="content-wrapper">
<h2>API Reference</h2>
<ul id="menu">
{{range .Sections}}
<li><a href="#{{.ID}}">{{.Title}}</a>
{{if .SubSections}}
<ul>
{{range .SubSections}}
<li><a href="#{{.ID}}">{{.Title}}</a></li>
{{end}}
</ul>
{{end}}
</li>
{{end}}
</ul>
</div>
</div>
<div class="column middle-column">
<div class="content-wrapper">
<h1>{{.Title}}</h1>
{{range .Sections}}
<section id="{{.ID}}">
<h2>{{.Title}}</h2>
{{.Content}}
{{range .SubSections}}
<section id="{{.ID}}">
<h3>{{.Title}}</h3>
{{.Content}}
</section>
{{end}}
</section>
{{end}}
</div>
</div>
<div class="column right-column">
<div class="content-wrapper">
<div class="language-switcher">
<button class="language-button active" data-language="php">PHP</button>
<button class="language-button" data-language="javascript">JavaScript</button>
<button class="language-button" data-language="go">Go</button>
</div>
<!-- Code examples will be populated here -->
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
// Language switcher
document.querySelectorAll('.language-button').forEach(button => {
button.addEventListener('click', () => {
const language = button.dataset.language;
// Update button states
document.querySelectorAll('.language-button').forEach(btn => {
btn.classList.remove('active');
});
button.classList.add('active');
// Update code block visibility
document.querySelectorAll('.code-block').forEach(block => {
block.classList.remove('active');
if (block.dataset.language === language) {
block.classList.add('active');
}
});
});
});
// Menu animation on scroll
const menu = document.getElementById('menu');
const menuItems = menu.getElementsByTagName('a');
const sections = document.querySelectorAll('section');
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
function setActiveMenuItem() {
let currentSection = '';
sections.forEach(section => {
if (isInViewport(section)) {
currentSection = section.id;
}
});
Array.from(menuItems).forEach(item => {
item.parentElement.classList.remove('active');
if (item.getAttribute('href').slice(1) === currentSection) {
item.parentElement.classList.add('active');
}
});
}
window.addEventListener('scroll', setActiveMenuItem);
setActiveMenuItem(); // Call once to set initial state
</script>
</body>
</html>
`
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: go run main.go input.md output.html")
os.Exit(1)
}
inputFile := os.Args[1]
outputFile := os.Args[2]
// Read the Markdown file
mdContent, err := ioutil.ReadFile(inputFile)
if err != nil {
log.Fatalf("Error reading Markdown file: %v", err)
}
// Convert Markdown to HTML
html := blackfriday.Run(mdContent)
// Parse the HTML and extract sections
pageData := parseHTML(html)
// Set the title to the input filename without extension
pageData.Title = strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile))
// Create a new template and parse the template string
tmpl, err := template.New("apiDocs").Parse(templateHTML)
if err != nil {
log.Fatalf("Error parsing template: %v", err)
}
// Create the output file
out, err := os.Create(outputFile)
if err != nil {
log.Fatalf("Error creating output file: %v", err)
}
defer out.Close()
// Execute the template and write to the output file
err = tmpl.Execute(out, pageData)
if err != nil {
log.Fatalf("Error executing template: %v", err)
}
fmt.Printf("Successfully generated %s\n", outputFile)
}
func parseHTML(html []byte) PageData {
var pageData PageData
var currentSection *Section
var currentSubSection *Section
reader := bytes.NewReader(html)
doc, err := blackfriday.Parse(reader.Bytes(), &blackfriday.Renderer{})
if err != nil {
log.Fatalf("Error parsing HTML: %v", err)
}
doc.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
if !entering {
return blackfriday.GoToNext
}
switch node.Type {
case blackfriday.Heading:
title := string(node.FirstChild.Literal)
id := strings.ToLower(strings.ReplaceAll(title, " ", "-"))
if node.Level == 2 {
currentSection = &Section{
ID: id,
Title: title,
}
pageData.Sections = append(pageData.Sections, *currentSection)
currentSubSection = nil
} else if node.Level == 3 && currentSection != nil {
currentSubSection = &Section{
ID: id,
Title: title,
}
currentSection.SubSections = append(currentSection.SubSections, *currentSubSection)
}
default:
if currentSubSection != nil {
currentSubSection.Content += template.HTML(blackfriday.Run(node.Literal))
} else if currentSection != nil {
currentSection.Content += template.HTML(blackfriday.Run(node.Literal))
}
}
return blackfriday.GoToNext
})
return pageData
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment