Add syntax highlighting

pull/7/head
Leonid Maslakov 1 year ago
parent 70cc0eb412
commit 45b2f21510

@ -20,5 +20,7 @@ updates:
schedule:
interval: "daily"
ignore:
- dependency-name: "github.com/alecthomas/chroma"
versions: ["2.x"]
- dependency-name: "github.com/mattn/go-sqlite3"
versions: ["2.x"]

@ -114,10 +114,7 @@ func main() {
TimeFormat: "2006/01/02 15:04:05",
}
apiv1Data := apiv1.Data{
DB: db,
Log: log,
}
apiv1Data := apiv1.Load(db, log)
rawData := raw.Data{
DB: db,

@ -4,4 +4,7 @@ go 1.11
replace git.lcomrade.su/root/lenpaste/internal => ./internal
require github.com/mattn/go-sqlite3 v1.14.13
require (
github.com/alecthomas/chroma v0.10.0
github.com/mattn/go-sqlite3 v1.14.13
)

@ -1,6 +1,11 @@
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/chroma/v2 v2.0.0-alpha4 h1:6s0y/julsg565meUfJd/aDv5nR4srI3Z3RgyId8w3Ro=
github.com/alecthomas/chroma/v2 v2.0.0-alpha4/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=

@ -21,9 +21,20 @@ package apiv1
import (
"git.lcomrade.su/root/lenpaste/internal/logger"
"git.lcomrade.su/root/lenpaste/internal/storage"
chromaLexers "github.com/alecthomas/chroma/lexers"
)
type Data struct {
Log logger.Config
DB storage.DB
Lexers []string
}
func Load(db storage.DB, log logger.Config) Data {
return Data{
DB: db,
Log: log,
Lexers: chromaLexers.Names(false),
}
}

@ -0,0 +1,70 @@
// Copyright (C) 2021-2022 Leonid Maslakov.
// This file is part of Lenpaste.
// Lenpaste is free software: you can redistribute it
// and/or modify it under the terms of the
// GNU Affero Public License as published by the
// Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
// Lenpaste is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero Public License for more details.
// You should have received a copy of the GNU Affero Public License along with Lenpaste.
// If not, see <https://www.gnu.org/licenses/>.
package apiv1
import (
"encoding/json"
"git.lcomrade.su/root/lenpaste/internal/netshare"
"git.lcomrade.su/root/lenpaste/internal/storage"
"net/http"
)
// GET /api/v1/getSafely
func (data Data) GetSafelyHand(rw http.ResponseWriter, req *http.Request) {
// Check method
if req.Method != "GET" {
data.writeError(rw, req, netshare.ErrBadRequest)
return
}
// Get paste ID
req.ParseForm()
pasteID := req.Form.Get("id")
// Check paste id
if pasteID == "" {
data.writeError(rw, req, netshare.ErrBadRequest)
return
}
// Get paste
paste, err := data.DB.PasteGet(pasteID)
if err != nil {
data.writeError(rw, req, err)
return
}
// If "one use" paste
if paste.OneUse == true {
paste = storage.Paste{
ID: paste.ID,
OneUse: paste.OneUse,
}
}
// Return response
rw.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(rw).Encode(paste)
if err != nil {
data.Log.HttpError(req, err)
return
}
}

@ -35,7 +35,7 @@ func (data Data) NewHand(rw http.ResponseWriter, req *http.Request) {
// Get form data and create paste
req.ParseForm()
paste, err := netshare.PasteAddFromForm(data.DB, req.PostForm)
paste, err := netshare.PasteAddFromForm(data.DB, data.Lexers, req.PostForm)
if err != nil {
if err == netshare.ErrBadRequest {
data.writeError(rw, req, netshare.ErrBadRequest)

@ -25,11 +25,12 @@ import (
"time"
)
func PasteAddFromForm(dbInfo storage.DB, form url.Values) (storage.Paste, error) {
func PasteAddFromForm(dbInfo storage.DB, lexerNames []string, form url.Values) (storage.Paste, error) {
// Read form
paste := storage.Paste{
Title: form.Get("title"),
Body: form.Get("body"),
Syntax: form.Get("syntax"),
DeleteTime: 0,
OneUse: false,
}
@ -39,6 +40,19 @@ func PasteAddFromForm(dbInfo storage.DB, form url.Values) (storage.Paste, error)
return paste, ErrBadRequest
}
// Check syntax
syntaxOk := false
for _, name := range lexerNames {
if name == paste.Syntax {
syntaxOk = true
break
}
}
if syntaxOk == false {
return paste, ErrBadRequest
}
// Get delete time
expirStr := form.Get("expiration")
if expirStr != "" {

@ -52,6 +52,7 @@ func (dbInfo DB) InitDB() error {
"id" TEXT PRIMARY KEY,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"syntax" TEXT NOT NULL,
"create_time" INTEGER NOT NULL,
"delete_time" INTEGER NOT NULL,
"one_use" BOOL NOT NULL

@ -30,7 +30,7 @@ type Paste struct {
CreateTime int64 `json:"createTime"` // Ignored when creating
DeleteTime int64 `json:"deleteTime"`
OneUse bool `json:"oneUse"`
//Syntax string `json:"syntax"`
Syntax string `json:"syntax"`
//Password string `json:"password"`
}
@ -58,8 +58,8 @@ func (dbInfo DB) PasteAdd(paste Paste) (Paste, error) {
// Add
_, err = db.Exec(
`INSERT INTO "pastes" ("id", "title", "body", "create_time", "delete_time", "one_use") VALUES (?, ?, ?, ?, ?, ?)`,
paste.ID, paste.Title, paste.Body, paste.CreateTime, paste.DeleteTime, paste.OneUse,
`INSERT INTO "pastes" ("id", "title", "body", "syntax", "create_time", "delete_time", "one_use") VALUES (?, ?, ?, ?, ?, ?, ?)`,
paste.ID, paste.Title, paste.Body, paste.Syntax, paste.CreateTime, paste.DeleteTime, paste.OneUse,
)
if err != nil {
return paste, err
@ -110,12 +110,12 @@ func (dbInfo DB) PasteGet(id string) (Paste, error) {
// Make query
row := db.QueryRow(
`SELECT "id", "title", "body", "create_time", "delete_time", "one_use" FROM "pastes" WHERE "id" = ?`,
`SELECT "id", "title", "body", "syntax", "create_time", "delete_time", "one_use" FROM "pastes" WHERE "id" = ?`,
id,
)
// Read query
err = row.Scan(&paste.ID, &paste.Title, &paste.Body, &paste.CreateTime, &paste.DeleteTime, &paste.OneUse)
err = row.Scan(&paste.ID, &paste.Title, &paste.Body, &paste.Syntax, &paste.CreateTime, &paste.DeleteTime, &paste.OneUse)
if err != nil {
if err == sql.ErrNoRows {
return paste, ErrNotFoundID
@ -139,7 +139,7 @@ func (dbInfo DB) PasteGetList() ([]Paste, error) {
// Make query
rows, err := db.Query(
`SELECT "id", "title", "body", "create_time", "delete_time", "one_use" FROM "pastes"`,
`SELECT "id", "title", "body", "syntax", "create_time", "delete_time", "one_use" FROM "pastes"`,
)
if err != nil {
if err == sql.ErrNoRows {
@ -153,7 +153,7 @@ func (dbInfo DB) PasteGetList() ([]Paste, error) {
for rows.Next() {
var paste Paste
err = rows.Scan(&paste.ID, &paste.Title, &paste.Body, &paste.CreateTime, &paste.DeleteTime, &paste.OneUse)
err = rows.Scan(&paste.ID, &paste.Title, &paste.Body, &paste.Syntax, &paste.CreateTime, &paste.DeleteTime, &paste.OneUse)
if err != nil {
return pastes, err
}

@ -21,6 +21,7 @@ package web
import (
"git.lcomrade.su/root/lenpaste/internal/logger"
"git.lcomrade.su/root/lenpaste/internal/storage"
chromaLexers "github.com/alecthomas/chroma/lexers"
"html/template"
"path/filepath"
)
@ -29,6 +30,8 @@ type Data struct {
DB storage.DB
Log logger.Config
Lexers []string
StyleCSS []byte
Main *template.Template
PastePage *template.Template
@ -49,6 +52,9 @@ func Load(webDir string, db storage.DB, log logger.Config) (Data, error) {
data.DB = db
data.Log = log
// Get Chroma lexers
data.Lexers = chromaLexers.Names(false)
// style.css file
data.StyleCSS, err = readFile(filepath.Join(webDir, "style.css"))
if err != nil {

@ -0,0 +1,63 @@
// Copyright (C) 2021-2022 Leonid Maslakov.
// This file is part of Lenpaste.
// Lenpaste is free software: you can redistribute it
// and/or modify it under the terms of the
// GNU Affero Public License as published by the
// Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
// Lenpaste is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero Public License for more details.
// You should have received a copy of the GNU Affero Public License along with Lenpaste.
// If not, see <https://www.gnu.org/licenses/>.
package web
import (
"bytes"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
)
func highlight(source string, lexer string) (string, error) {
// Determine lexer
l := lexers.Get(lexer)
if l == nil {
return source, nil
}
l = chroma.Coalesce(l)
// Determine formatter
f := html.New(
html.Standalone(false),
html.WithClasses(false),
html.TabWidth(4),
html.WithLineNumbers(true),
html.WrapLongLines(true),
)
s := styles.Get("dracula")
it, err := l.Tokenise(nil, source)
if err != nil {
return "", err
}
// Format
var buf bytes.Buffer
err = f.Format(&buf, s, it)
if err != nil {
return "", err
}
return buf.String(), nil
}

@ -20,6 +20,7 @@ package web
import (
"git.lcomrade.su/root/lenpaste/internal/storage"
"html/template"
"net/http"
"time"
)
@ -27,7 +28,7 @@ import (
type pasteTmpl struct {
ID string
Title string
Body string
Body template.HTML
CreateTime int64
DeleteTime int64
OneUse bool
@ -75,6 +76,13 @@ func (data Data) getPaste(rw http.ResponseWriter, req *http.Request) {
}
}
//Highlight body
bodyHighlight, err := highlight(paste.Body, "go")
if err != nil {
data.errorInternal(rw, req, err)
return
}
// Prepare template data
createTime := time.Unix(paste.CreateTime, 0).UTC()
deleteTime := time.Unix(paste.DeleteTime, 0).UTC()
@ -82,7 +90,7 @@ func (data Data) getPaste(rw http.ResponseWriter, req *http.Request) {
tmplData := pasteTmpl{
ID: paste.ID,
Title: paste.Title,
Body: paste.Body,
Body: template.HTML(bodyHighlight),
CreateTime: paste.CreateTime,
DeleteTime: paste.DeleteTime,
OneUse: paste.OneUse,

@ -23,13 +23,17 @@ import (
"net/http"
)
type createTmpl struct {
Lexers []string
}
func (data Data) newPaste(rw http.ResponseWriter, req *http.Request) {
// Read request
req.ParseForm()
if req.PostForm.Get("body") != "" {
// Create paste
paste, err := netshare.PasteAddFromForm(data.DB, req.PostForm)
paste, err := netshare.PasteAddFromForm(data.DB, data.Lexers, req.PostForm)
if err != nil {
if err == netshare.ErrBadRequest {
data.errorBadRequest(rw, req)
@ -46,9 +50,13 @@ func (data Data) newPaste(rw http.ResponseWriter, req *http.Request) {
}
// Else show create page
tmplData := createTmpl{
Lexers: data.Lexers,
}
rw.Header().Set("Content-Type", "text/html")
err := data.Main.Execute(rw, "")
err := data.Main.Execute(rw, tmplData)
if err != nil {
data.errorInternal(rw, req, err)
return

@ -12,11 +12,22 @@
autocomplete="off" autocorrect="off" spellcheck="true"
rows=20 wrap="off" tabindex=2 required
></textarea></div>
<div><input type="checkbox" name="oneUse" value="true" tabindex=3>Burn after reading</input></div>
<div>
<label for="syntax">Syntax:</label>
<select name="syntax" tabindex=3 size=1>
{{range .Lexers}}
<option value="{{.}}"{{if eq . "plaintext"}} selected="true"{{end}}>{{.}}</option>
{{end}}
</select>
</div>
<div>
<label class="checkbox">
<input type="checkbox" name="oneUse" value="true" tabindex=4></input>Burn after reading</label>
</div>
<div>
<label for="expiration">Expiration:</label>
<select name="expiration" tabindex=4 size=1>
<option value="0" selected>Never</option>
<select name="expiration" tabindex=5 size=1>
<option value="0" selected="true">Never</option>
<option value="600">10 minutes</option>
<option value="1800">30 minutes</option>
<option value="3600">1 hour</option>
@ -32,6 +43,6 @@
<option value="31557600">1 Year</option>
</select>
</div>
<div><button class="button-green" type="submit" tabindex=5>Create paste</button></div>
<div><button class="button-green" type="submit" tabindex=6>Create paste</button></div>
</form>
{{end}}

@ -7,7 +7,7 @@
<p style="text-align: right;"><a style="button" href="/raw/{{.ID}}" tabindex=2>Raw</a> <a href="/dl/{{.ID}}" tabindex=3>Download</a></p>
{{end}}
<textarea wrap="off" rows=25 autofocus tabindex=2 readonly>{{.Body}}</textarea>
{{.Body}}
<p>Created: {{.CreateTimeStr}}</p>

@ -159,6 +159,11 @@ button:hover {
background: #74D117;
}
.checkbox:hover,
.checkbox:hover input {
cursor: pointer;
}
ul {
list-style: square;
}

Loading…
Cancel
Save