Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,30 @@ You can also specify a port:
go-grip -p 80 README.md
```

or just open a file-tree with all available files in the current directory:
Or preview the current directory. If `README.md` exists, go-grip opens that page first:

```bash
go-grip -r=false
go-grip .
```

It's also possible to activate the darkmode:
To keep the preview stable while editing and refresh manually in the browser:

```bash
go-grip -d .
go-grip --no-reload README.md
```

To disable automatic browser reload on file changes (useful for stable editing):
To disable automatic browser reload and MathJax rendering together:

```bash
go-grip --no-reload README.md
go-grip --no-reload --disable-markdown-feature mathjax README.md
```

To disable markdown features in the renderer, use `--disable-markdown-feature`.
Supported feature names are `details`, `footnote`, `ghissue`, `mathjax`, and `mermaid`.

```bash
go-grip --disable-markdown-feature mathjax README.md
go-grip --disable-markdown-feature mathjax --disable-markdown-feature mermaid README.md
```

To terminate the current server simply press `CTRL-C`.
Expand Down
13 changes: 12 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"os"
"strings"

"github.com/chrishrb/go-grip/internal"
"github.com/spf13/cobra"
Expand All @@ -16,14 +17,19 @@ var rootCmd = &cobra.Command{
host, _ := cmd.Flags().GetString("host")
port, _ := cmd.Flags().GetInt("port")
boundingBox, _ := cmd.Flags().GetBool("bounding-box")
disabledFeatures, _ := cmd.Flags().GetStringSlice("disable-markdown-feature")
noReload, _ := cmd.Flags().GetBool("no-reload")

var file string
if len(args) == 1 {
file = args[0]
}

parser := internal.NewParser()
if err := internal.ValidateMarkdownFeatures(disabledFeatures); err != nil {
return err
}

parser := internal.NewParser(disabledFeatures)
server := internal.NewServer(host, port, boundingBox, browser, !noReload, parser)
return server.Serve(file)
},
Expand All @@ -41,5 +47,10 @@ func init() {
rootCmd.Flags().StringP("host", "H", "localhost", "Host to use")
rootCmd.Flags().IntP("port", "p", 6419, "Port to use")
rootCmd.Flags().Bool("bounding-box", true, "Add bounding box to HTML")
rootCmd.Flags().StringSlice(
"disable-markdown-feature",
nil,
"Disable optional markdown feature(s): "+strings.Join(internal.SupportedMarkdownFeatures(), ", "),
)
rootCmd.Flags().Bool("no-reload", false, "Disable automatic browser reload on file changes")
}
8 changes: 8 additions & 0 deletions defaults/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@
<style id="highlight-light" media="(prefers-color-scheme: light)">{{ .CssCodeLight }}</style>
<style id="highlight-dark" media="(prefers-color-scheme: dark)">{{ .CssCodeDark }}</style>
<link rel="stylesheet" href="/static/css/github-print.css" media="print" />
{{if index .Features "mermaid"}}
<link rel="stylesheet" href="/static/css/github-mermaid.css" />
{{end}}
<link rel="stylesheet" href="/static/css/github-clipboard.css" />
{{if index .Features "mathjax"}}
<link rel="stylesheet" href="/static/css/mathjax.css" />
{{end}}
<link rel="stylesheet" href="/static/css/theme-switch.css" />
<script src="/static/js/theme-switch.js"></script>
<script src="/static/js/clipboard-copy.js"></script>
{{if index .Features "mathjax"}}
<script src="/static/js/mathjax-options.js"></script>
<script src="/static/js/tex-mml-chtml.js"></script>
{{end}}
{{if index .Features "mermaid"}}
<script src="/static/js/mermaid.min.js"></script>
<script src="/static/js/mermaid-init.js"></script>
{{end}}
</head>

<body class="markdown-body">
Expand Down
112 changes: 94 additions & 18 deletions internal/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package internal

import (
"bytes"
"fmt"
"slices"
"strings"

"github.com/chrishrb/go-grip/pkg/alert"
"github.com/chrishrb/go-grip/pkg/details"
Expand All @@ -19,29 +22,102 @@ import (
"go.abhg.dev/goldmark/mermaid"
)

type Parser struct{}
const (
FeatureDetails = "details"
FeatureFootnote = "footnote"
FeatureGHIssue = "ghissue"
FeatureMathJax = "mathjax"
FeatureMermaid = "mermaid"
)

var supportedMarkdownFeatures = []string{
FeatureDetails,
FeatureFootnote,
FeatureGHIssue,
FeatureMathJax,
FeatureMermaid,
}

type Parser struct {
disabledFeatures map[string]struct{}
}

func NewParser(disabledFeatures []string) *Parser {
// Normalize feature names once so the rest of the parser can use simple lookups.
disabled := make(map[string]struct{}, len(disabledFeatures))
for _, feature := range disabledFeatures {
disabled[strings.ToLower(feature)] = struct{}{}
}
return &Parser{disabledFeatures: disabled}
}

func SupportedMarkdownFeatures() []string {
return slices.Clone(supportedMarkdownFeatures)
}

func NewParser() *Parser {
return &Parser{}
func ValidateMarkdownFeatures(features []string) error {
var invalid []string
for _, feature := range features {
feature = strings.ToLower(strings.TrimSpace(feature))
if !slices.Contains(supportedMarkdownFeatures, feature) {
invalid = append(invalid, feature)
}
}
if len(invalid) == 0 {
return nil
}

return fmt.Errorf(
"unknown markdown feature(s): %s (supported: %s)",
strings.Join(invalid, ", "),
strings.Join(supportedMarkdownFeatures, ", "),
)
}

func (m Parser) FeatureEnabled(name string) bool {
_, disabled := m.disabledFeatures[strings.ToLower(name)]
return !disabled
}

func (m Parser) TemplateFeatures() map[string]bool {
// Keep template asset toggles derived from the same feature registry as parsing.
features := make(map[string]bool, len(supportedMarkdownFeatures))
for _, feature := range supportedMarkdownFeatures {
features[feature] = m.FeatureEnabled(feature)
}
return features
}

func (m Parser) MdToHTML(input []byte) ([]byte, error) {
// Always-on GitHub-style extensions stay in the base list; optional ones are gated below.
extensions := []goldmark.Extender{
extension.Linkify,
extension.Table,
extension.Strikethrough,
tasklist.TaskList,
emoji.Emoji,
&hashtag.Extender{},
alert.New(),
highlighting.Highlighting,
}
if m.FeatureEnabled(FeatureFootnote) {
extensions = append(extensions, footnote.Footnote)
}
if m.FeatureEnabled(FeatureMermaid) {
extensions = append(extensions, &mermaid.Extender{RenderMode: mermaid.RenderModeClient, NoScript: true})
}
if m.FeatureEnabled(FeatureMathJax) {
extensions = append(extensions, mathjax.MathJax)
}
if m.FeatureEnabled(FeatureGHIssue) {
extensions = append(extensions, ghissue.New())
}
if m.FeatureEnabled(FeatureDetails) {
extensions = append(extensions, details.New())
}

md := goldmark.New(
goldmark.WithExtensions(
extension.Linkify,
extension.Table,
extension.Strikethrough,
footnote.Footnote,
tasklist.TaskList,
emoji.Emoji,
&hashtag.Extender{},
alert.New(),
highlighting.Highlighting,
&mermaid.Extender{RenderMode: mermaid.RenderModeClient, NoScript: true},
mathjax.MathJax,
ghissue.New(),
details.New(),
),
goldmark.WithExtensions(extensions...),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
Expand Down
105 changes: 105 additions & 0 deletions internal/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package internal

import (
"strings"
"testing"
)

func TestParserFeatureDisablePassThrough(t *testing.T) {
t.Parallel()

tests := []struct {
name string
disabledFeature string
input string
wantContains []string
wantNotContains []string
}{
{
name: "mathjax",
disabledFeature: FeatureMathJax,
input: "Inline math: $x + y$\n",
wantContains: []string{"$x + y$"},
wantNotContains: []string{`\(`, `\)`},
},
{
name: "mermaid",
disabledFeature: FeatureMermaid,
input: "```mermaid\ngraph TD;\n A-->B;\n```\n",
wantContains: []string{`class="highlight`, `graph TD;`, `A--&gt;B;`},
wantNotContains: []string{`<pre class="mermaid">`},
},
{
name: "details",
disabledFeature: FeatureDetails,
input: "<details><summary>Open me</summary>Inside details</details>\n",
wantContains: []string{`<details><summary>Open me</summary>Inside details</details>`},
wantNotContains: []string{`id="details-`, `sessionStorage`},
},
{
name: "footnote",
disabledFeature: FeatureFootnote,
input: "Footnote ref[^1].\n\n[^1]: Footnote text.\n",
wantContains: []string{`Footnote ref[^1].`, `[^1]: Footnote text.`},
wantNotContains: []string{`class="footnote-ref"`, `class="footnotes"`},
},
{
name: "ghissue",
disabledFeature: FeatureGHIssue,
input: "Issue refs: #46 and grafana/grafana#22\n",
wantContains: []string{`Issue refs: #46 and grafana/grafana#22`},
wantNotContains: []string{`class="issue-link"`},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

parser := NewParser([]string{tt.disabledFeature})
html, err := parser.MdToHTML([]byte(tt.input))
if err != nil {
t.Fatalf("MdToHTML returned error: %v", err)
}

got := string(html)
for _, want := range tt.wantContains {
if !strings.Contains(got, want) {
t.Fatalf("expected output to contain %q, got %q", want, got)
}
}
for _, unwanted := range tt.wantNotContains {
if strings.Contains(got, unwanted) {
t.Fatalf("expected output not to contain %q, got %q", unwanted, got)
}
}
})
}
}

func TestParserMathEnabledTransformsInlineMath(t *testing.T) {
t.Parallel()

parser := NewParser(nil)
html, err := parser.MdToHTML([]byte("Inline math: $x + y$\n"))
if err != nil {
t.Fatalf("MdToHTML returned error: %v", err)
}

got := string(html)
if !strings.Contains(got, `\(`) || !strings.Contains(got, `\)`) {
t.Fatalf("expected mathjax delimiters in output, got %q", got)
}
if strings.Contains(got, "$x + y$") {
t.Fatalf("expected inline math to be transformed, got %q", got)
}
}

func TestValidateMarkdownFeaturesRejectsUnknownValues(t *testing.T) {
t.Parallel()

err := ValidateMarkdownFeatures([]string{"mathjax", "bogus"})
if err == nil {
t.Fatal("expected validation error for unknown feature")
}
}
2 changes: 2 additions & 0 deletions internal/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func (s *Server) newHandler(dir http.Dir) http.Handler {
BoundingBox: s.boundingBox,
CssCodeLight: getCssCode("github"),
CssCodeDark: getCssCode("github-dark"),
Features: s.parser.TemplateFeatures(),
})
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -159,6 +160,7 @@ type htmlStruct struct {
BoundingBox bool
CssCodeLight string
CssCodeDark string
Features map[string]bool
}

func serveTemplate(w http.ResponseWriter, html htmlStruct) error {
Expand Down
Loading