diff --git a/fluffy/templates/markdown.html b/fluffy/templates/markdown.html
deleted file mode 100644
index 09c3429..0000000
--- a/fluffy/templates/markdown.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% set page_name = 'markdown' %}
-{% extends 'layouts/text.html' %}
-
-{% block extra_head %}
-
-{% endblock %}
-
-{% block info %}
- {{num_lines(text)}} {{'line'|pluralize(num_lines(text))}} of Markdown
-{% endblock info %}
-
-{% block text %}
-
-
- {{text|markdown|safe}}
-
-
-{% endblock %}
-
-{% block inline_js %}{% endblock %}
diff --git a/go.mod b/go.mod
index 8145b3d..ccb8927 100644
--- a/go.mod
+++ b/go.mod
@@ -31,5 +31,6 @@ require (
github.com/aws/smithy-go v1.20.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
+ github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/sys v0.24.0 // indirect
)
diff --git a/go.sum b/go.sum
index 7779b97..f64539a 100644
--- a/go.sum
+++ b/go.sum
@@ -54,6 +54,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
diff --git a/scss/markdown.scss b/scss/markdown.scss
deleted file mode 100644
index 6d78f81..0000000
--- a/scss/markdown.scss
+++ /dev/null
@@ -1,104 +0,0 @@
-$source-code-font: 'Source Code Pro', monospace;
-
-.page-markdown {
- #container {
- width: 960px;
- }
-
- .text {
- background-color: white !important;
- font-size: 15px;
-
- padding: 24px;
- line-height: 1.5em;
- color: #111;
-
- & > *:first-child {
- margin-top: 0 !important;
- }
-
- h1, h2, h3, h4, h5, h6 {
- font-weight: bold;
- margin-top: 26px;
- }
-
- h1 { font-size: 28px; }
- h1, h2 {
- border-bottom: solid 1px #eee;
- padding-bottom: 16px;
- margin-bottom: 14px;
- }
- h2 { font-size: 24px; }
- h3 { font-size: 18px; }
- h4 { font-size: 16px; }
- h5 { font-size: 14px; }
- h6 { font-size: 14px; color: #777; }
-
- blockquote {
- color: #555;
- padding: 0 15px;
- border-left: solid 5px #ddd;
- }
-
- $source-code-bg-color: #f6f6f6;
-
- pre {
- font-family: $source-code-font;
- overflow-x: auto;
- background-color: $source-code-bg-color;
- padding: 14px;
- margin-bottom: 10px;
- }
-
- code {
- font-family: $source-code-font;
- padding: 3px 6px;
- font-size: 12px;
- background-color: $source-code-bg-color;
- border-radius: 2px;
- }
-
- a {
- text-decoration: none;
-
- &:hover {
- text-decoration: underline;
- }
- }
- }
-
- ul, ol {
- padding-left: 35px;
-
- li {
- display: list-item;
- margin: 5px 0;
- }
- }
-
- ul {
- list-style: disc;
- }
-
- ol {
- list-style: decimal;
-
- li {
- padding-left: 5px;
- }
- }
-
- td, th {
- border: solid 1px #ddd;
- padding: 8px;
- }
-
- th {
- font-weight: bold;
- background-color: #f1f1f1;
- }
-
- img {
- max-width: 100%;
- }
-}
diff --git a/scss/paste.scss b/scss/paste.scss
index 04feed9..ce295ff 100644
--- a/scss/paste.scss
+++ b/scss/paste.scss
@@ -1,3 +1,6 @@
+$source-code-font: 'Source Code Pro', monospace;
+$paste-padding-top: 2px;
+
.page-paste {
#style {
float: right;
@@ -7,6 +10,15 @@
display: none;
}
}
+
+ &.markdown {
+ #container {
+ width: 960px;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+
#container {
width: auto;
@@ -24,51 +36,23 @@
}
}
- #paste {
- font-family: 'Source Code Pro', monospace;
-
- $paste-padding-top: 2px;
-
- .line-numbers {
- padding: $paste-padding-top 0;
- /* color comes from the pygments styles */
- border-right-width: 1px;
- border-right-style: solid;
-
- float: left;
- line-height: 1.25em;
- text-align: right;
-
-
- a {
- cursor: pointer;
- display: block;
- text-decoration: none;
- padding-left: 8px;
- padding-right: 5px;
- }
- }
-
- .text {
- padding: $paste-padding-top 0;
- overflow-x: auto;
-
- .highlight {
- // Allow width to expand to match content so block element
- // children (e.g. lines) extend the full width.
- display: inline-block;
- min-width: 100%;
- }
+ .line-numbers {
+ font-family: $source-code-font;
+ padding: $paste-padding-top 0;
+ /* color comes from the pygments styles */
+ border-right-width: 1px;
+ border-right-style: solid;
- .highlight > pre {
- line-height: 125%;
+ float: left;
+ line-height: 1.25em;
+ text-align: right;
- // These are line spans (sadly, no class is output by pygments)
- & > span {
- padding-left: 5px;
- display: block;
- }
- }
+ a {
+ cursor: pointer;
+ display: block;
+ text-decoration: none;
+ padding-left: 8px;
+ padding-right: 5px;
}
}
@@ -92,4 +76,124 @@
}
}
}
+
+ .text.plain-text {
+ font-family: $source-code-font;
+ padding: $paste-padding-top 0;
+ overflow-x: auto;
+
+ .highlight {
+ // Allow width to expand to match content so block element
+ // children (e.g. lines) extend the full width.
+ display: inline-block;
+ min-width: 100%;
+ }
+
+ .highlight > pre {
+ line-height: 125%;
+
+ // These are line spans (sadly, no class is output by pygments)
+ & > span {
+ padding-left: 5px;
+ display: block;
+ }
+ }
+ }
+
+ .text.rich-text {
+ background-color: white !important;
+ font-size: 15px;
+
+ padding: 24px;
+ line-height: 1.5em;
+ color: #111;
+
+ & > *:first-child {
+ margin-top: 0 !important;
+ }
+
+ h1, h2, h3, h4, h5, h6 {
+ font-weight: bold;
+ margin-top: 26px;
+ }
+
+ h1 { font-size: 28px; }
+ h1, h2 {
+ border-bottom: solid 1px #eee;
+ padding-bottom: 16px;
+ margin-bottom: 14px;
+ }
+ h2 { font-size: 24px; }
+ h3 { font-size: 18px; }
+ h4 { font-size: 16px; }
+ h5 { font-size: 14px; }
+ h6 { font-size: 14px; color: #777; }
+
+ blockquote {
+ color: #555;
+ padding: 0 15px;
+ border-left: solid 5px #ddd;
+ }
+
+ $source-code-bg-color: #f6f6f6;
+
+ pre {
+ font-family: $source-code-font;
+ overflow-x: auto;
+ background-color: $source-code-bg-color;
+ padding: 14px;
+ margin-bottom: 10px;
+ }
+
+ code {
+ font-family: $source-code-font;
+ padding: 3px 6px;
+ font-size: 12px;
+ background-color: $source-code-bg-color;
+ border-radius: 2px;
+ }
+
+ a {
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ ul, ol {
+ padding-left: 35px;
+
+ li {
+ display: list-item;
+ margin: 5px 0;
+ }
+ }
+
+ ul {
+ list-style: disc;
+ }
+
+ ol {
+ list-style: decimal;
+
+ li {
+ padding-left: 5px;
+ }
+ }
+
+ td, th {
+ border: solid 1px #ddd;
+ padding: 8px;
+ }
+
+ th {
+ font-weight: bold;
+ background-color: #f1f1f1;
+ }
+
+ img {
+ max-width: 100%;
+ }
+ }
}
diff --git a/server/highlighting/highlighting.go b/server/highlighting/highlighting.go
index d4498c9..a198f8f 100644
--- a/server/highlighting/highlighting.go
+++ b/server/highlighting/highlighting.go
@@ -57,22 +57,93 @@ var Styles = []StyleCategory{
},
}
+// Text represents a single piece of text.
+//
+// A single paste usually consists of a single Text, but in the case of a diff, it may contain
+// multiple Texts.
+type Text struct {
+ Text string
+ // Array index corresponds to zero-indexed line number in this text,
+ // and the value is the array of zero-indexed line numbers that line
+ // corresponds to in the original text.
+ LineNumberMapping [][]int
+}
+
+func simpleText(text string) *Text {
+ // This is a little tricky because whether lines end in newlines depends on the source of the
+ // text. Our approach is to not count a final \n if it's present since the user probably
+ // doesn't intend to create an empty line at the end of the text. We do count multiple newlines
+ // though since that is more clear.
+ // "a" => 1 line
+ // "a\n" => 1 line
+ // "a\nb" => 2 lines
+ // "a\nb\n" => 2 lines
+ // "a\nb\n\n" => 3 lines
+ lineCount := strings.Count(text, "\n") + 1
+ if strings.HasSuffix(text, "\n") {
+ lineCount -= 1
+ }
+ mapping := make([][]int, lineCount)
+ for i := range mapping {
+ mapping[i] = []int{i}
+ }
+ return &Text{
+ Text: text,
+ LineNumberMapping: mapping,
+ }
+}
+
+// Highlighter is an interface for syntax highlighting.
+//
+// A Highlighter is responsible for taking a piece of text and returning an HTML representation.
type Highlighter interface {
+ // Name returns the name of the highlighter (e.g. "Python").
Name() string
- IsDiff() bool
+ // RenderAsDiff returns true if the paste should be rendered as a diff.
+ RenderAsDiff() bool
+ // RenderAsRichText returns true if the paste should be rendered as rich text, without line
+ // numbers or other plaintext formatting. Useful for e.g. Markdown.
+ RenderAsRichText() bool
+ // ExtraHTMLClasses returns a extra CSS classes that should be added.
+ ExtraHTMLClasses() []string
+ // GenerateTexts takes a piece of text and returns a slice of Texts.
+ // Generally this will return a single Text, but in the case of a diff, it may return multiple.
+ GenerateTexts(text string) []*Text
+ // Highlight takes a piece of text and returns an HTML representation.
Highlight(text *Text) (template.HTML, error)
}
+func GuessHighlighterForPaste(text string, language string) Highlighter {
+ if language == "rendered-markdown" {
+ return &MarkdownHighlighter{}
+ }
+
+ // TODO: implement
+ return &PlainTextHighlighter{}
+}
+
type PlainTextHighlighter struct{}
func (p *PlainTextHighlighter) Name() string {
return "Plain Text"
}
-func (p *PlainTextHighlighter) IsDiff() bool {
+func (p *PlainTextHighlighter) RenderAsDiff() bool {
+ return false
+}
+
+func (p *PlainTextHighlighter) RenderAsRichText() bool {
return false
}
+func (p *PlainTextHighlighter) ExtraHTMLClasses() []string {
+ return nil
+}
+
+func (p *PlainTextHighlighter) GenerateTexts(text string) []*Text {
+ return []*Text{simpleText(text)}
+}
+
func (p *PlainTextHighlighter) Highlight(text *Text) (template.HTML, error) {
var html strings.Builder
for _, line := range strings.Split(text.Text, "\n") {
@@ -81,11 +152,3 @@ func (p *PlainTextHighlighter) Highlight(text *Text) (template.HTML, error) {
}
return template.HTML(html.String()), nil
}
-
-type Text struct {
- Text string
- // Array index corresponds to zero-indexed line number in this text,
- // and the value is the array of zero-indexed line numbers that line
- // corresponds to in the original text.
- LineNumberMapping [][]int
-}
diff --git a/server/highlighting/markdown.go b/server/highlighting/markdown.go
new file mode 100644
index 0000000..1e8acce
--- /dev/null
+++ b/server/highlighting/markdown.go
@@ -0,0 +1,50 @@
+package highlighting
+
+import (
+ "fmt"
+ "html/template"
+ "strings"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/parser"
+)
+
+type MarkdownHighlighter struct{}
+
+func (m *MarkdownHighlighter) Name() string {
+ return "Rendered Markdown"
+}
+
+func (m *MarkdownHighlighter) RenderAsDiff() bool {
+ return false
+}
+
+func (m *MarkdownHighlighter) RenderAsRichText() bool {
+ return true
+}
+
+func (p *MarkdownHighlighter) ExtraHTMLClasses() []string {
+ return []string{"markdown"}
+}
+
+func (p *MarkdownHighlighter) GenerateTexts(text string) []*Text {
+ return []*Text{simpleText(text)}
+}
+
+func (m *MarkdownHighlighter) Highlight(text *Text) (template.HTML, error) {
+ md := goldmark.New(
+ // TODO: add syntax highlighting
+ goldmark.WithExtensions(extension.GFM),
+ goldmark.WithParserOptions(
+ parser.WithAutoHeadingID(),
+ ),
+ )
+
+ var html strings.Builder
+ if err := md.Convert([]byte(text.Text), &html); err != nil {
+ return "", fmt.Errorf("rendering Markdown: %w", err)
+ }
+
+ return template.HTML(html.String()), nil
+}
diff --git a/server/security/csp.go b/server/security/csp.go
index f52cb3f..c5f0035 100644
--- a/server/security/csp.go
+++ b/server/security/csp.go
@@ -40,10 +40,18 @@ func NewCSPMiddleware(conf *config.Config, logger logging.Logger, next http.Hand
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
csp := strings.Builder{}
- fmt.Fprintf(&csp, "default-src 'self' %s; script-src https://ajax.googleapis.com %[1]s ", fileURLBase)
+ // default-src
+ fmt.Fprintf(&csp, "default-src 'self' %s", fileURLBase)
if isDevStaticFileRequest(conf, r) {
- fmt.Fprintf(&csp, "'unsafe-inline'")
+ // Needed for embedded images in rendered markdown dev pastes.
+ fmt.Fprintf(&csp, " *")
+ }
+
+ // script-src
+ fmt.Fprintf(&csp, "; script-src https://ajax.googleapis.com %s", fileURLBase)
+ if isDevStaticFileRequest(conf, r) {
+ fmt.Fprintf(&csp, " 'unsafe-inline'")
} else {
nonceBytes := make([]byte, 16)
if _, err := rand.Read(nonceBytes); err != nil {
@@ -53,10 +61,14 @@ func NewCSPMiddleware(conf *config.Config, logger logging.Logger, next http.Hand
}
nonce := hex.EncodeToString(nonceBytes)
ctx = context.WithValue(ctx, cspNonceKey{}, nonce)
- fmt.Fprintf(&csp, "'nonce-%s'", nonce)
+ fmt.Fprintf(&csp, " 'nonce-%s'", nonce)
}
- fmt.Fprintf(&csp, "; style-src 'self' https://fonts.googleapis.com %s; font-src https://fonts.gstatic.com %[1]s", fileURLBase)
+ // style-src
+ fmt.Fprintf(&csp, "; style-src 'self' https://fonts.googleapis.com %s", fileURLBase)
+
+ // font-src
+ fmt.Fprintf(&csp, "; font-src https://fonts.gstatic.com %s", fileURLBase)
w.Header().Set("Content-Security-Policy", csp.String())
next.ServeHTTP(w, r.WithContext(ctx))
diff --git a/server/security/csp_test.go b/server/security/csp_test.go
index 058cdac..c6f114f 100644
--- a/server/security/csp_test.go
+++ b/server/security/csp_test.go
@@ -31,7 +31,7 @@ var (
cspDevStaticFileRegexp = regexp.MustCompile(
strings.Join(
[]string{
- `default-src 'self' https://fancy-cdn.com`,
+ `default-src 'self' https://fancy-cdn.com \*`,
`script-src https://ajax.googleapis.com https://fancy-cdn.com 'unsafe-inline'`,
`style-src 'self' https://fonts.googleapis.com https://fancy-cdn.com`,
`font-src https://fonts.gstatic.com https://fancy-cdn.com`,
diff --git a/server/templates/include/base.html b/server/templates/include/base.html
index 0a7d592..f624be0 100644
--- a/server/templates/include/base.html
+++ b/server/templates/include/base.html
@@ -40,6 +40,3 @@