mirror of
https://github.com/writefreely/writefreely
synced 2024-11-10 11:24:13 +00:00
merge develop
This commit is contained in:
commit
f2e3cd8bd7
52 changed files with 17914 additions and 168 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
node_modules
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -1,17 +1,19 @@
|
|||
# Build image
|
||||
FROM golang:1.13-alpine as build
|
||||
FROM golang:1.14-alpine as build
|
||||
|
||||
RUN apk add --update nodejs nodejs-npm make g++ git sqlite-dev
|
||||
RUN apk add --update nodejs nodejs-npm make g++ git
|
||||
RUN npm install -g less less-plugin-clean-css
|
||||
RUN go get -u github.com/jteeuwen/go-bindata/...
|
||||
RUN go get -u github.com/go-bindata/go-bindata/...
|
||||
|
||||
RUN mkdir -p /go/src/github.com/writeas/writefreely
|
||||
WORKDIR /go/src/github.com/writeas/writefreely
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV GO111MODULE=on
|
||||
|
||||
RUN make build \
|
||||
&& make ui
|
||||
&& make ui
|
||||
RUN mkdir /stage && \
|
||||
cp -R /go/bin \
|
||||
/go/src/github.com/writeas/writefreely/templates \
|
||||
|
@ -22,7 +24,7 @@ RUN mkdir /stage && \
|
|||
/stage
|
||||
|
||||
# Final image
|
||||
FROM alpine:3.11
|
||||
FROM alpine:3.12
|
||||
|
||||
RUN apk add --no-cache openssl ca-certificates
|
||||
COPY --from=build --chown=daemon:daemon /stage /go
|
||||
|
|
1
Makefile
1
Makefile
|
@ -131,6 +131,7 @@ release-docker :
|
|||
|
||||
ui : force_look
|
||||
cd less/; $(MAKE) $(MFLAGS)
|
||||
cd prose/; $(MAKE) $(MFLAGS)
|
||||
|
||||
assets : generate
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
|
||||
|
|
10
account.go
10
account.go
|
@ -151,8 +151,6 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
}
|
||||
|
||||
// Handle empty optional params
|
||||
// TODO: remove this var
|
||||
createdWithPass := true
|
||||
hashedPass, err := auth.HashPass([]byte(signup.Pass))
|
||||
if err != nil {
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
|
||||
|
@ -162,7 +160,7 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
u := &User{
|
||||
Username: signup.Alias,
|
||||
HashedPass: hashedPass,
|
||||
HasPass: createdWithPass,
|
||||
HasPass: true,
|
||||
Email: prepareUserEmail(signup.Email, app.keys.EmailKey),
|
||||
Created: time.Now().Truncate(time.Second).UTC(),
|
||||
}
|
||||
|
@ -188,9 +186,6 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
resUser := &AuthUser{
|
||||
User: u,
|
||||
}
|
||||
if !createdWithPass {
|
||||
resUser.Password = signup.Pass
|
||||
}
|
||||
title := signup.Alias
|
||||
if signup.Normalize {
|
||||
title = desiredUsername
|
||||
|
@ -826,6 +821,9 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
|||
return ErrCollectionNotFound
|
||||
}
|
||||
|
||||
// Add collection properties
|
||||
c.MonetizationPointer = app.db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
|
||||
silenced, err := app.db.IsUserSilenced(u.ID)
|
||||
if err != nil {
|
||||
log.Error("view edit collection %v", err)
|
||||
|
|
1
admin.go
1
admin.go
|
@ -532,6 +532,7 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt
|
|||
}
|
||||
apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
|
||||
apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
|
||||
apper.App().cfg.App.Monetization = r.FormValue("monetization") == "on"
|
||||
apper.App().cfg.App.Private = r.FormValue("private") == "on"
|
||||
apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
|
||||
if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
|
||||
|
|
|
@ -56,6 +56,8 @@ type (
|
|||
PublicOwner bool `datastore:"public_owner" json:"-"`
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
MonetizationPointer string `json:"monetization_pointer,omitempty"`
|
||||
|
||||
db *datastore
|
||||
hostName string
|
||||
}
|
||||
|
@ -87,14 +89,15 @@ type (
|
|||
Handle string `schema:"handle" json:"handle"`
|
||||
|
||||
// Actual collection values updated in the DB
|
||||
Alias *string `schema:"alias" json:"alias"`
|
||||
Title *string `schema:"title" json:"title"`
|
||||
Description *string `schema:"description" json:"description"`
|
||||
StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"`
|
||||
Script *sql.NullString `schema:"script" json:"script"`
|
||||
Signature *sql.NullString `schema:"signature" json:"signature"`
|
||||
Visibility *int `schema:"visibility" json:"public"`
|
||||
Format *sql.NullString `schema:"format" json:"format"`
|
||||
Alias *string `schema:"alias" json:"alias"`
|
||||
Title *string `schema:"title" json:"title"`
|
||||
Description *string `schema:"description" json:"description"`
|
||||
StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"`
|
||||
Script *sql.NullString `schema:"script" json:"script"`
|
||||
Signature *sql.NullString `schema:"signature" json:"signature"`
|
||||
Monetization *string `schema:"monetization_pointer" json:"monetization_pointer"`
|
||||
Visibility *int `schema:"visibility" json:"public"`
|
||||
Format *sql.NullString `schema:"format" json:"format"`
|
||||
}
|
||||
CollectionFormat struct {
|
||||
Format string
|
||||
|
@ -552,6 +555,7 @@ type CollectionPage struct {
|
|||
IsOwner bool
|
||||
CanPin bool
|
||||
Username string
|
||||
Monetization string
|
||||
Collections *[]Collection
|
||||
PinnedPosts *[]PublicPost
|
||||
IsAdmin bool
|
||||
|
@ -723,14 +727,14 @@ func newDisplayCollection(c *Collection, cr *collectionReq, page int) *DisplayCo
|
|||
return coll
|
||||
}
|
||||
|
||||
// getCollectionPage returns the collection page as an int. If the parsed page value is not
|
||||
// greater than 0 then the default value of 1 is returned.
|
||||
func getCollectionPage(vars map[string]string) int {
|
||||
page := 1
|
||||
var p int
|
||||
p, _ = strconv.Atoi(vars["page"])
|
||||
if p > 0 {
|
||||
page = p
|
||||
if p, _ := strconv.Atoi(vars["page"]); p > 0 {
|
||||
return p
|
||||
}
|
||||
return page
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// handleViewCollection displays the requested Collection
|
||||
|
@ -829,6 +833,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
// Add more data
|
||||
// TODO: fix this mess of collections inside collections
|
||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner)
|
||||
displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer")
|
||||
|
||||
collTmpl := "collection"
|
||||
if app.cfg.App.Chorus {
|
||||
|
@ -947,6 +952,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
|
|||
// Add more data
|
||||
// TODO: fix this mess of collections inside collections
|
||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner)
|
||||
displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer")
|
||||
|
||||
err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
[server]
|
||||
hidden_host =
|
||||
port = 8080
|
||||
|
||||
[database]
|
||||
type = mysql
|
||||
username = root
|
||||
password = changeme
|
||||
database = writefreely
|
||||
host = db
|
||||
port = 3306
|
||||
tls = false
|
||||
|
||||
[app]
|
||||
site_name = WriteFreely Example Blog!
|
||||
host = http://localhost:8080
|
||||
theme = write
|
||||
disable_js = false
|
||||
webfonts = true
|
||||
single_user = true
|
||||
open_registration = false
|
||||
min_username_len = 3
|
||||
max_blogs = 1
|
||||
federation = true
|
||||
public_stats = true
|
||||
private = false
|
||||
update_checks = true
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -108,6 +108,7 @@ type (
|
|||
TokenEndpoint string `ini:"token_endpoint"`
|
||||
InspectEndpoint string `ini:"inspect_endpoint"`
|
||||
AuthEndpoint string `ini:"auth_endpoint"`
|
||||
Scope string `ini:"scope"`
|
||||
AllowDisconnect bool `ini:"allow_disconnect"`
|
||||
}
|
||||
|
||||
|
@ -137,9 +138,12 @@ type (
|
|||
MinUsernameLen int `ini:"min_username_len"`
|
||||
MaxBlogs int `ini:"max_blogs"`
|
||||
|
||||
// Options for public instances
|
||||
// Federation
|
||||
Federation bool `ini:"federation"`
|
||||
PublicStats bool `ini:"public_stats"`
|
||||
Federation bool `ini:"federation"`
|
||||
PublicStats bool `ini:"public_stats"`
|
||||
Monetization bool `ini:"monetization"`
|
||||
NotesOnly bool `ini:"notes_only"`
|
||||
|
||||
// Access
|
||||
Private bool `ini:"private"`
|
||||
|
|
45
database.go
45
database.go
|
@ -905,6 +905,29 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
}
|
||||
}
|
||||
|
||||
// Update Monetization value
|
||||
if c.Monetization != nil {
|
||||
skipUpdate := false
|
||||
if *c.Monetization != "" {
|
||||
// Strip away any excess spaces
|
||||
trimmed := strings.TrimSpace(*c.Monetization)
|
||||
// Only update value when it starts with "$", per spec: https://paymentpointers.org
|
||||
if strings.HasPrefix(trimmed, "$") {
|
||||
c.Monetization = &trimmed
|
||||
} else {
|
||||
// Value appears invalid, so don't update
|
||||
skipUpdate = true
|
||||
}
|
||||
}
|
||||
if !skipUpdate {
|
||||
_, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?", collID, "monetization_pointer", *c.Monetization, *c.Monetization)
|
||||
if err != nil {
|
||||
log.Error("Unable to insert monetization_pointer value: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update rest of the collection data
|
||||
res, err = db.Exec("UPDATE collections SET "+q.Updates+" WHERE "+q.Conditions, q.Params...)
|
||||
if err != nil {
|
||||
|
@ -2162,6 +2185,28 @@ func (db *datastore) CollectionHasAttribute(id int64, attr string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (db *datastore) GetCollectionAttribute(id int64, attr string) string {
|
||||
var v string
|
||||
err := db.QueryRow("SELECT value FROM collectionattributes WHERE collection_id = ? AND attribute = ?", id, attr).Scan(&v)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return ""
|
||||
case err != nil:
|
||||
log.Error("Couldn't SELECT value in getCollectionAttribute for attribute '%s': %v", attr, err)
|
||||
return ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (db *datastore) SetCollectionAttribute(id int64, attr, v string) error {
|
||||
_, err := db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?)", id, attr, v)
|
||||
if err != nil {
|
||||
log.Error("Unable to INSERT into collectionattributes: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAccount will delete the entire account for userID
|
||||
func (db *datastore) DeleteAccount(userID int64) error {
|
||||
// Get all collections
|
||||
|
|
|
@ -1,32 +1,47 @@
|
|||
version: "3"
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
volumes:
|
||||
- "web-data:/go/src/app"
|
||||
- "./config.ini.example:/go/src/app/config.ini"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- writefreely
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: "mariadb:latest"
|
||||
volumes:
|
||||
- "./schema.sql:/tmp/schema.sql"
|
||||
- db-data:/var/lib/mysql/data
|
||||
networks:
|
||||
- writefreely
|
||||
environment:
|
||||
- MYSQL_DATABASE=writefreely
|
||||
- MYSQL_ROOT_PASSWORD=changeme
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
web-data:
|
||||
web-keys:
|
||||
db-data:
|
||||
|
||||
networks:
|
||||
writefreely:
|
||||
external_writefreely:
|
||||
internal_writefreely:
|
||||
internal: true
|
||||
|
||||
services:
|
||||
writefreely-web:
|
||||
container_name: "writefreely-web"
|
||||
image: "writefreely:latest"
|
||||
|
||||
volumes:
|
||||
- "web-keys:/go/keys"
|
||||
- "./config.ini:/go/config.ini"
|
||||
|
||||
networks:
|
||||
- "internal_writefreely"
|
||||
- "external_writefreely"
|
||||
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
depends_on:
|
||||
- "writefreely-db"
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
writefreely-db:
|
||||
container_name: "writefreely-db"
|
||||
image: "mariadb:latest"
|
||||
|
||||
volumes:
|
||||
- "db-data:/var/lib/mysql/data"
|
||||
|
||||
networks:
|
||||
- "internal_writefreely"
|
||||
|
||||
environment:
|
||||
- MYSQL_DATABASE=writefreely
|
||||
- MYSQL_ROOT_PASSWORD=changeme
|
||||
|
||||
restart: unless-stopped
|
||||
|
|
4
feed.go
4
feed.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -104,7 +104,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
|||
Title: title,
|
||||
Link: &Link{Href: permalink},
|
||||
Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>",
|
||||
Content: applyMarkdown([]byte(p.Content), "", app.cfg),
|
||||
Content: string(p.HTMLContent),
|
||||
Author: &Author{author, ""},
|
||||
Created: p.Created,
|
||||
Updated: p.Updated,
|
||||
|
|
16
go.mod
16
go.mod
|
@ -3,12 +3,12 @@ module github.com/writeas/writefreely
|
|||
require (
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/fatih/color v1.10.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/go-test/deep v1.0.1 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/schema v1.2.0
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/guregu/null v3.5.0+incompatible
|
||||
|
@ -17,18 +17,18 @@ require (
|
|||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec
|
||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||
github.com/manifoldco/promptui v0.7.0
|
||||
github.com/mattn/go-sqlite3 v1.14.2
|
||||
github.com/manifoldco/promptui v0.8.0
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/microcosm-cc/bluemonday v1.0.4
|
||||
github.com/mitchellh/go-wordwrap v1.0.0
|
||||
github.com/mitchellh/go-wordwrap v1.0.1
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
github.com/writeas/activity v0.1.2
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
|
@ -44,7 +44,7 @@ require (
|
|||
github.com/writefreely/go-nodeinfo v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
|
||||
gopkg.in/ini.v1 v1.57.0
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
|
51
go.sum
51
go.sum
|
@ -1,8 +1,6 @@
|
|||
code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs=
|
||||
code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
|
@ -29,8 +27,8 @@ github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0
|
|||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
|
@ -50,8 +48,9 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
|||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
|
@ -81,22 +80,21 @@ github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec/go.mod h1:
|
|||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4=
|
||||
github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
|
||||
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-sqlite3 v1.14.2 h1:A2EQLwjYf/hfYaM20FVjs1UewCTTFR7RmjEHkLjldIA=
|
||||
github.com/mattn/go-sqlite3 v1.14.2/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
|
@ -117,10 +115,10 @@ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpke
|
|||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY=
|
||||
github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0=
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 h1:BiSivIxLQFcKoUorpNN3rNwwFG5bITPnqUSyIccfdh0=
|
||||
|
@ -160,19 +158,16 @@ golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnf
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
|
@ -181,10 +176,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -5,6 +5,7 @@ all :
|
|||
lessc app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css
|
||||
lessc fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css
|
||||
lessc icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css
|
||||
lessc prose.less --clean-css="--s1 --advanced" $(CSSDIR)prose.css
|
||||
|
||||
install :
|
||||
./install-less.sh
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
@import "admin";
|
||||
@import "login";
|
||||
@import "pages/error";
|
||||
@import "resources";
|
||||
@import "lib/elements";
|
||||
@import "lib/material";
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
@primary: rgb(114, 120, 191);
|
||||
@secondary: rgb(114, 191, 133);
|
||||
@subheaders: #444;
|
||||
@headerTextColor: black;
|
||||
@sansFont: 'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
@serifFont: Lora, 'Palatino Linotype', 'Book Antiqua', 'New York', 'DejaVu serif', serif;
|
||||
@monoFont: Hack, consolas, Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace, monospace;
|
||||
@dangerCol: #e21d27;
|
||||
@errUrgentCol: #ecc63c;
|
||||
@proSelectedCol: #71D571;
|
||||
@textLinkColor: rgb(0, 0, 238);
|
||||
|
||||
@accent: #767676;
|
||||
|
||||
body {
|
||||
font-family: @serifFont;
|
||||
font-size-adjust: 0.5;
|
||||
|
@ -529,7 +515,7 @@ pre, body#post article, #post .alert, #subpage .alert, body#collection article,
|
|||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article p {
|
||||
textarea, input#title, pre, body#post article, body#collection article p {
|
||||
&.norm, &.sans, &.wrap {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap; /* CSS 3 */
|
||||
|
@ -539,7 +525,7 @@ textarea, pre, body#post article, body#collection article p {
|
|||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article, body#subpage article, span, .font {
|
||||
textarea, input#title, pre, body#post article, body#collection article, body#subpage article, span, .font {
|
||||
&.norm {
|
||||
font-family: @serifFont;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Install Less via npm
|
||||
if [ ! -e "$(which lessc)" ]; then
|
||||
sudo npm install -g less
|
||||
sudo npm install -g less@3.5.3
|
||||
sudo npm install -g less-plugin-clean-css
|
||||
else
|
||||
echo LESS $(npm view less version 2>&1 | grep -v WARN) is installed
|
||||
|
|
|
@ -188,18 +188,18 @@ body#pad, body#pad-sub {
|
|||
body#pad {
|
||||
.pad-theme-transition;
|
||||
|
||||
textarea {
|
||||
textarea, #title {
|
||||
.pad-theme-transition;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
textarea {
|
||||
textarea, #title, #editor {
|
||||
background-color: @darkBG;
|
||||
color: @darkTextColor;
|
||||
}
|
||||
}
|
||||
&.light {
|
||||
textarea {
|
||||
textarea, #title, #editor {
|
||||
background-color: @lightBG;
|
||||
color: @lightTextColor;
|
||||
}
|
||||
|
|
|
@ -256,7 +256,7 @@ body#pad {
|
|||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
textarea {
|
||||
textarea, #title {
|
||||
position: fixed !important;
|
||||
top: 3em;
|
||||
right: 0;
|
||||
|
@ -385,6 +385,14 @@ body#pad .alert {
|
|||
top: 2.25em;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
&.classic {
|
||||
#editor {
|
||||
top: 5.25em;
|
||||
}
|
||||
#title {
|
||||
top: 3.5rem;
|
||||
}
|
||||
}
|
||||
#tools {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
@ -438,8 +446,8 @@ body#pad .alert {
|
|||
}
|
||||
|
||||
@media all and (min-width: 50em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
@ -450,8 +458,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 60em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 15%;
|
||||
padding-right: 15%;
|
||||
}
|
||||
|
@ -462,8 +470,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 70em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
}
|
||||
|
@ -474,8 +482,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 85em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 25%;
|
||||
padding-right: 25%;
|
||||
}
|
||||
|
@ -486,8 +494,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 105em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 30%;
|
||||
padding-right: 30%;
|
||||
}
|
||||
|
|
450
less/prose-editor.less
Normal file
450
less/prose-editor.less
Normal file
|
@ -0,0 +1,450 @@
|
|||
body#pad.classic {
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
#editor {
|
||||
top: 4em;
|
||||
}
|
||||
#title {
|
||||
top: 4.25rem;
|
||||
bottom: unset;
|
||||
height: auto;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border: 0;
|
||||
}
|
||||
#tools {
|
||||
#belt {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
#target {
|
||||
ul {
|
||||
a {
|
||||
padding: 0 0.5em !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
height: calc(~"100% - 1.6em");
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
font-size: 1.2em;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
padding: 0.5em 0;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::-moz-selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-selectednode {
|
||||
outline: 2px solid #8cf;
|
||||
}
|
||||
|
||||
/* Make sure li selections wrap around markers */
|
||||
|
||||
li.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
li.ProseMirror-selectednode:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
right: -2px;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
border: 2px solid #8cf;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ProseMirror-textblock-dropdown {
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu {
|
||||
margin: 0 -4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ProseMirror-tooltip .ProseMirror-menu {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
margin-right: 3px;
|
||||
display: inline-block;
|
||||
div {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menuseparator {
|
||||
border-right: 1px solid #ddd;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown {
|
||||
vertical-align: 1px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-wrap {
|
||||
padding: 1px 0 1px 4px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown:after {
|
||||
content: "";
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 2px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
color: #666;
|
||||
border: 1px solid #aaa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
z-index: 15;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item {
|
||||
cursor: pointer;
|
||||
padding: 2px 8px 2px 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item:hover {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap {
|
||||
position: relative;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label:after {
|
||||
content: "";
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu {
|
||||
display: none;
|
||||
min-width: 4em;
|
||||
left: 100%;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
position: relative;
|
||||
min-height: 1em;
|
||||
color: #666;
|
||||
padding: 0.5em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 10;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
display: inline-block;
|
||||
line-height: .8;
|
||||
vertical-align: -2px; /* Compensate for padding */
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-icon svg {
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.ProseMirror-icon span {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid black;
|
||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes ProseMirror-cursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Add space around the hr to make clicking it easier */
|
||||
|
||||
.ProseMirror-example-setup-style hr {
|
||||
padding: 2px 10px;
|
||||
border: none;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style hr:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: silver;
|
||||
line-height: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror ul, .ProseMirror ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.ProseMirror blockquote {
|
||||
padding-left: 1em;
|
||||
border-left: 3px solid #eee;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style img {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
background: white;
|
||||
padding: 1em;
|
||||
border: 1px solid silver;
|
||||
position: fixed;
|
||||
border-radius: 0.25em;
|
||||
z-index: 11;
|
||||
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.ProseMirror-prompt h5 {
|
||||
margin: 0 0 0.75em;
|
||||
font-family: @sansFont;
|
||||
font-size: 100%;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"],
|
||||
.ProseMirror-prompt textarea {
|
||||
background: #eee;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"] {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
color: #666;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close:after {
|
||||
content: "✕";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ProseMirror-invalid {
|
||||
background: #ffc;
|
||||
border: 1px solid #cc7;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-buttons {
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#editor, .editor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: black;
|
||||
background-clip: padding-box;
|
||||
padding: 5px 0;
|
||||
margin: 4em auto 23px auto;
|
||||
}
|
||||
|
||||
.ProseMirror p:first-child,
|
||||
.ProseMirror h1:first-child,
|
||||
.ProseMirror h2:first-child,
|
||||
.ProseMirror h3:first-child,
|
||||
.ProseMirror h4:first-child,
|
||||
.ProseMirror h5:first-child,
|
||||
.ProseMirror h6:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 123px;
|
||||
border: 1px solid silver;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
padding: 3px 10px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper, #markdown textarea {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.editorreadmore {
|
||||
color: @textLinkColor;
|
||||
text-decoration: underline;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 50em) {
|
||||
#editor {
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 60em) {
|
||||
#editor {
|
||||
margin-left: 15%;
|
||||
margin-right: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 70em) {
|
||||
#editor {
|
||||
margin-left: 20%;
|
||||
margin-right: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 85em) {
|
||||
#editor {
|
||||
margin-left: 25%;
|
||||
margin-right: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 105em) {
|
||||
#editor {
|
||||
margin-left: 30%;
|
||||
margin-right: 30%;
|
||||
}
|
||||
}
|
4
less/prose.less
Normal file
4
less/prose.less
Normal file
|
@ -0,0 +1,4 @@
|
|||
@import "prose-editor";
|
||||
@import "pad-theme";
|
||||
@import "resources";
|
||||
@import "lib/elements";
|
13
less/resources.less
Normal file
13
less/resources.less
Normal file
|
@ -0,0 +1,13 @@
|
|||
@primary: rgb(114, 120, 191);
|
||||
@secondary: rgb(114, 191, 133);
|
||||
@subheaders: #444;
|
||||
@headerTextColor: black;
|
||||
@sansFont: 'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
@serifFont: Lora, 'Palatino Linotype', 'Book Antiqua', 'New York', 'DejaVu serif', serif;
|
||||
@monoFont: Hack, consolas, Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace, monospace;
|
||||
@dangerCol: #e21d27;
|
||||
@errUrgentCol: #ecc63c;
|
||||
@proSelectedCol: #71D571;
|
||||
@textLinkColor: rgb(0, 0, 238);
|
||||
|
||||
@accent: #767676;
|
1
oauth.go
1
oauth.go
|
@ -265,6 +265,7 @@ func configureGenericOauth(parentHandler *Handler, r *mux.Router, app *App) {
|
|||
AuthLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.AuthEndpoint,
|
||||
HttpClient: config.DefaultHTTPClient(),
|
||||
CallbackLocation: callbackLocation,
|
||||
Scope: config.OrDefaultString(app.Config().GenericOauth.Scope, "read_user"),
|
||||
}
|
||||
configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ type genericOauthClient struct {
|
|||
ExchangeLocation string
|
||||
InspectLocation string
|
||||
CallbackLocation string
|
||||
Scope string
|
||||
HttpClient HttpClient
|
||||
}
|
||||
|
||||
|
@ -46,7 +47,7 @@ func (c genericOauthClient) buildLoginURL(state string) (string, error) {
|
|||
q.Set("redirect_uri", c.CallbackLocation)
|
||||
q.Set("response_type", "code")
|
||||
q.Set("state", state)
|
||||
q.Set("scope", "read_user")
|
||||
q.Set("scope", c.Scope)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
@ -55,7 +56,7 @@ func (c genericOauthClient) exchangeOauthCode(ctx context.Context, code string)
|
|||
form := url.Values{}
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("redirect_uri", c.CallbackLocation)
|
||||
form.Add("scope", "read_user")
|
||||
form.Add("scope", c.Scope)
|
||||
form.Add("code", code)
|
||||
req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
|
@ -110,5 +111,6 @@ func (c genericOauthClient) inspectOauthAccessToken(ctx context.Context, accessT
|
|||
if inspectResponse.Error != "" {
|
||||
return nil, errors.New(inspectResponse.Error)
|
||||
}
|
||||
|
||||
return &inspectResponse, nil
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"html"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
@ -73,6 +74,25 @@ func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string {
|
|||
return applyMarkdownSpecial(data, false, baseURL, cfg)
|
||||
}
|
||||
|
||||
func disableYoutubeAutoplay(outHTML string) string {
|
||||
for _, match := range youtubeReg.FindAllString(outHTML, -1) {
|
||||
u, err := url.Parse(match)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
u.RawQuery = html.UnescapeString(u.RawQuery)
|
||||
q := u.Query()
|
||||
// Set Youtube autoplay url parameter, if any, to 0
|
||||
if len(q["autoplay"]) == 1 {
|
||||
q.Set("autoplay", "0")
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
cleanURL := u.String()
|
||||
outHTML = strings.Replace(outHTML, match, cleanURL, 1)
|
||||
}
|
||||
return outHTML
|
||||
}
|
||||
|
||||
func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string {
|
||||
mdExtensions := 0 |
|
||||
blackfriday.EXTENSION_TABLES |
|
||||
|
@ -108,10 +128,7 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *c
|
|||
// Strip newlines on certain block elements that render with them
|
||||
outHTML = blockReg.ReplaceAllString(outHTML, "<$1>")
|
||||
outHTML = endBlockReg.ReplaceAllString(outHTML, "</$1></$2>")
|
||||
// Remove all query parameters on YouTube embed links
|
||||
// TODO: make this more specific. Taking the nuclear approach here to strip ?autoplay=1
|
||||
outHTML = youtubeReg.ReplaceAllString(outHTML, "$1")
|
||||
|
||||
outHTML = disableYoutubeAutoplay(outHTML)
|
||||
return outHTML
|
||||
}
|
||||
|
||||
|
@ -140,9 +157,7 @@ func applyBasicMarkdown(data []byte) string {
|
|||
func postTitle(content, friendlyId string) string {
|
||||
const maxTitleLen = 80
|
||||
|
||||
// Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
|
||||
// entities added in by sanitizing the content.
|
||||
content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
|
||||
content = stripHTMLWithoutEscaping(content)
|
||||
|
||||
content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace)
|
||||
eol := strings.IndexRune(content, '\n')
|
||||
|
@ -160,9 +175,7 @@ func postTitle(content, friendlyId string) string {
|
|||
func friendlyPostTitle(content, friendlyId string) string {
|
||||
const maxTitleLen = 80
|
||||
|
||||
// Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
|
||||
// entities added in by sanitizing the content.
|
||||
content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
|
||||
content = stripHTMLWithoutEscaping(content)
|
||||
|
||||
content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace)
|
||||
eol := strings.IndexRune(content, '\n')
|
||||
|
@ -179,6 +192,12 @@ func friendlyPostTitle(content, friendlyId string) string {
|
|||
return title
|
||||
}
|
||||
|
||||
// Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
|
||||
// entities added in by sanitizing the content.
|
||||
func stripHTMLWithoutEscaping(content string) string {
|
||||
return html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content))
|
||||
}
|
||||
|
||||
func getSanitizationPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
policy.AllowAttrs("src", "style").OnElements("iframe", "video", "audio")
|
||||
|
|
12
posts.go
12
posts.go
|
@ -211,8 +211,7 @@ func (p Post) Summary() string {
|
|||
if p.Content == "" {
|
||||
return ""
|
||||
}
|
||||
// Strip out HTML
|
||||
p.Content = bluemonday.StrictPolicy().Sanitize(p.Content)
|
||||
p.Content = stripHTMLWithoutEscaping(p.Content)
|
||||
// and Markdown
|
||||
p.Content = stripmd.Strip(p.Content)
|
||||
|
||||
|
@ -1132,7 +1131,12 @@ func (p *PublicPost) CanonicalURL(hostName string) string {
|
|||
|
||||
func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
||||
cfg := app.cfg
|
||||
o := activitystreams.NewArticleObject()
|
||||
var o *activitystreams.Object
|
||||
if cfg.App.NotesOnly || strings.Index(p.Content, "\n\n") == -1 {
|
||||
o = activitystreams.NewNoteObject()
|
||||
} else {
|
||||
o = activitystreams.NewArticleObject()
|
||||
}
|
||||
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
|
||||
o.Published = p.Created
|
||||
o.URL = p.CanonicalURL(cfg.App.Host)
|
||||
|
@ -1480,6 +1484,7 @@ Are you sure it was ever here?`,
|
|||
IsOwner bool
|
||||
IsPinned bool
|
||||
IsCustomDomain bool
|
||||
Monetization string
|
||||
PinnedPosts *[]PublicPost
|
||||
IsFound bool
|
||||
IsAdmin bool
|
||||
|
@ -1497,6 +1502,7 @@ Are you sure it was ever here?`,
|
|||
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)
|
||||
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, p.IsOwner)
|
||||
tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p)
|
||||
tp.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer")
|
||||
|
||||
if !postFound {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
|
35
posts_test.go
Normal file
35
posts_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package writefreely_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/guregu/null/zero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/writeas/writefreely"
|
||||
)
|
||||
|
||||
func TestPostSummary(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
given writefreely.Post
|
||||
expected string
|
||||
}{
|
||||
"no special chars": {givenPost("Content."), "Content."},
|
||||
"HTML content": {givenPost("Content <p>with a</p> paragraph."), "Content with a paragraph."},
|
||||
"content with escaped char": {givenPost("Content's all OK."), "Content's all OK."},
|
||||
"multiline content": {givenPost(`Content
|
||||
in
|
||||
multiple
|
||||
lines.`), "Content in multiple lines."},
|
||||
}
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
actual := test.given.Summary()
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func givenPost(content string) writefreely.Post {
|
||||
return writefreely.Post{Title: zero.StringFrom("Title"), Content: content}
|
||||
}
|
8
prose/.babelrc.js
Normal file
8
prose/.babelrc.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
"presets": [
|
||||
["@babel/env", {
|
||||
"modules": false
|
||||
}]
|
||||
],
|
||||
"plugins": ["@babel/plugin-syntax-dynamic-import"]
|
||||
}
|
4
prose/.prettierrc
Normal file
4
prose/.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
3
prose/Makefile
Normal file
3
prose/Makefile
Normal file
|
@ -0,0 +1,3 @@
|
|||
all :
|
||||
npm install
|
||||
npm run-script build
|
7
prose/README.md
Normal file
7
prose/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Building
|
||||
|
||||
* Run `npm install` to download dependencies.
|
||||
* Run `npm run-script build` to build a production script in `../static/js/` or run
|
||||
`npm run develop` to build and watch for changes. You can use `prose.html`
|
||||
to test your development changes.
|
||||
* Manually copy the file `prose.bundle.js` to `static/js/`. _To be automated_
|
57
prose/markdownParser.js
Normal file
57
prose/markdownParser.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { MarkdownParser } from "prosemirror-markdown";
|
||||
import markdownit from "markdown-it";
|
||||
|
||||
import { writeFreelySchema } from "./schema";
|
||||
|
||||
export const writeAsMarkdownParser = new MarkdownParser(
|
||||
writeFreelySchema,
|
||||
markdownit("commonmark", { html: true }),
|
||||
{
|
||||
// blockquote: { block: "blockquote" },
|
||||
paragraph: { block: "paragraph" },
|
||||
list_item: { block: "list_item" },
|
||||
bullet_list: { block: "bullet_list" },
|
||||
ordered_list: {
|
||||
block: "ordered_list",
|
||||
getAttrs: (tok) => ({ order: +tok.attrGet("start") || 1 }),
|
||||
},
|
||||
heading: {
|
||||
block: "heading",
|
||||
getAttrs: (tok) => ({ level: +tok.tag.slice(1) }),
|
||||
},
|
||||
code_block: { block: "code_block", noCloseToken: true },
|
||||
fence: {
|
||||
block: "code_block",
|
||||
getAttrs: (tok) => ({ params: tok.info || "" }),
|
||||
noCloseToken: true,
|
||||
},
|
||||
// hr: { node: "horizontal_rule" },
|
||||
image: {
|
||||
node: "image",
|
||||
getAttrs: (tok) => ({
|
||||
src: tok.attrGet("src"),
|
||||
title: tok.attrGet("title") || null,
|
||||
alt: tok.children?.[0].content || null,
|
||||
}),
|
||||
},
|
||||
hardbreak: { node: "hard_break" },
|
||||
|
||||
em: { mark: "em" },
|
||||
strong: { mark: "strong" },
|
||||
link: {
|
||||
mark: "link",
|
||||
getAttrs: (tok) => ({
|
||||
href: tok.attrGet("href"),
|
||||
title: tok.attrGet("title") || null,
|
||||
}),
|
||||
},
|
||||
code_inline: { mark: "code", noCloseToken: true },
|
||||
html_block: {
|
||||
node: "readmore",
|
||||
getAttrs(token) {
|
||||
// TODO: Give different attributes depending on the token content
|
||||
return {};
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
123
prose/markdownSerializer.js
Normal file
123
prose/markdownSerializer.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { MarkdownSerializer } from "prosemirror-markdown";
|
||||
|
||||
function backticksFor(node, side) {
|
||||
const ticks = /`+/g;
|
||||
let m;
|
||||
let len = 0;
|
||||
if (node.isText)
|
||||
while ((m = ticks.exec(node.text))) len = Math.max(len, m[0].length);
|
||||
let result = len > 0 && side > 0 ? " `" : "`";
|
||||
for (let i = 0; i < len; i++) result += "`";
|
||||
if (len > 0 && side < 0) result += " ";
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPlainURL(link, parent, index, side) {
|
||||
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;
|
||||
const content = parent.child(index + (side < 0 ? -1 : 0));
|
||||
if (
|
||||
!content.isText ||
|
||||
content.text != link.attrs.href ||
|
||||
content.marks[content.marks.length - 1] != link
|
||||
)
|
||||
return false;
|
||||
if (index == (side < 0 ? 1 : parent.childCount - 1)) return true;
|
||||
const next = parent.child(index + (side < 0 ? -2 : 1));
|
||||
return !link.isInSet(next.marks);
|
||||
}
|
||||
|
||||
export const writeAsMarkdownSerializer = new MarkdownSerializer(
|
||||
{
|
||||
readmore(state, node) {
|
||||
state.write("<!--more-->\n");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
// blockquote(state, node) {
|
||||
// state.wrapBlock("> ", undefined, node, () => state.renderContent(node));
|
||||
// },
|
||||
code_block(state, node) {
|
||||
state.write(`\`\`\`${node.attrs.params || ""}\n`);
|
||||
state.text(node.textContent, false);
|
||||
state.ensureNewLine();
|
||||
state.write("```");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
heading(state, node) {
|
||||
state.write(`${state.repeat("#", node.attrs.level)} `);
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
bullet_list(state, node) {
|
||||
state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `);
|
||||
},
|
||||
ordered_list(state, node) {
|
||||
const start = node.attrs.order || 1;
|
||||
const maxW = String(start + node.childCount - 1).length;
|
||||
const space = state.repeat(" ", maxW + 2);
|
||||
state.renderList(node, space, (i) => {
|
||||
const nStr = String(start + i);
|
||||
return `${state.repeat(" ", maxW - nStr.length) + nStr}. `;
|
||||
});
|
||||
},
|
||||
list_item(state, node) {
|
||||
state.renderContent(node);
|
||||
},
|
||||
paragraph(state, node) {
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
|
||||
image(state, node) {
|
||||
state.write(
|
||||
`![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${
|
||||
node.attrs.title ? ` ${state.quote(node.attrs.title)}` : ""
|
||||
})`
|
||||
);
|
||||
},
|
||||
hard_break(state, node, parent, index) {
|
||||
for (let i = index + 1; i < parent.childCount; i += 1)
|
||||
if (parent.child(i).type !== node.type) {
|
||||
state.write("\\\n");
|
||||
return;
|
||||
}
|
||||
},
|
||||
text(state, node) {
|
||||
state.text(node.text || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
em: {
|
||||
open: "*",
|
||||
close: "*",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
strong: {
|
||||
open: "**",
|
||||
close: "**",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
link: {
|
||||
open(_state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, 1) ? "<" : "[";
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, -1)
|
||||
? ">"
|
||||
: `](${state.esc(mark.attrs.href)}${
|
||||
mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ""
|
||||
})`;
|
||||
},
|
||||
},
|
||||
code: {
|
||||
open(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index), -1);
|
||||
},
|
||||
close(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index - 1), 1);
|
||||
},
|
||||
escape: false,
|
||||
},
|
||||
}
|
||||
);
|
32
prose/menu.js
Normal file
32
prose/menu.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { MenuItem } from "prosemirror-menu";
|
||||
import { buildMenuItems } from "prosemirror-example-setup";
|
||||
|
||||
import { writeFreelySchema } from "./schema";
|
||||
|
||||
function canInsert(state, nodeType, attrs) {
|
||||
let $from = state.selection.$from;
|
||||
for (let d = $from.depth; d >= 0; d--) {
|
||||
let index = $from.index(d);
|
||||
if ($from.node(d).canReplaceWith(index, index, nodeType, attrs))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ReadMoreItem = new MenuItem({
|
||||
label: "Read more",
|
||||
select: (state) => canInsert(state, writeFreelySchema.nodes.readmore),
|
||||
run(state, dispatch) {
|
||||
dispatch(
|
||||
state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create())
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const getMenu = () => {
|
||||
const menuContent = [
|
||||
...buildMenuItems(writeFreelySchema).fullMenu,
|
||||
[ReadMoreItem],
|
||||
];
|
||||
return menuContent;
|
||||
};
|
16278
prose/package-lock.json
generated
Normal file
16278
prose/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
prose/package.json
Normal file
32
prose/package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "prose",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "prose.js",
|
||||
"dependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"markdown-it": "^12.0.4",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-keymap": "^1.1.4",
|
||||
"prosemirror-markdown": "github:VV-EE/prosemirror-markdown",
|
||||
"prosemirror-model": "^1.9.1",
|
||||
"prosemirror-state": "^1.3.2",
|
||||
"prosemirror-view": "^1.14.2",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.7",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"prettier": "^2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"develop": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
14
prose/prose.html
Normal file
14
prose/prose.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<link rel="stylesheet" href="../static/css/prose.css" />
|
||||
<div id="editor" style="margin-bottom: 0"></div>
|
||||
<!-- <div style="text-align: center"> -->
|
||||
<!-- <label style="border-right: 1px solid silver"> -->
|
||||
<!-- Markdown <input type=radio name=inputformat value=markdown> </label> -->
|
||||
<!-- <label> <input type=radio name=inputformat value=prosemirror checked> WYSIWYM</label> -->
|
||||
<!-- </div> -->
|
||||
|
||||
<div style="display: none">
|
||||
<textarea id="content">
|
||||
This is a comment written in [Markdown](http://commonmark.org). *You* may know the syntax for inserting a link, but does your whole audience? So you can give people the **choice** to use a more familiar, discoverable interface.</textarea
|
||||
>
|
||||
</div>
|
||||
<script src="dist/prose.bundle.js"></script>
|
118
prose/prose.js
Normal file
118
prose/prose.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
// class MarkdownView {
|
||||
// constructor(target, content) {
|
||||
// this.textarea = target.appendChild(document.createElement("textarea"))
|
||||
// this.textarea.value = content
|
||||
// }
|
||||
|
||||
// get content() { return this.textarea.value }
|
||||
// focus() { this.textarea.focus() }
|
||||
// destroy() { this.textarea.remove() }
|
||||
// }
|
||||
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { EditorState, TextSelection } from "prosemirror-state";
|
||||
import { exampleSetup } from "prosemirror-example-setup";
|
||||
import { keymap } from "prosemirror-keymap";
|
||||
|
||||
import { writeAsMarkdownParser } from "./markdownParser";
|
||||
import { writeAsMarkdownSerializer } from "./markdownSerializer";
|
||||
import { writeFreelySchema } from "./schema";
|
||||
import { getMenu } from "./menu";
|
||||
|
||||
let $title = document.querySelector("#title");
|
||||
let $content = document.querySelector("#content");
|
||||
|
||||
// Bugs:
|
||||
// 1. When there's just an empty line and a hard break is inserted with shift-enter then two enters are inserted
|
||||
// which do not show up in the markdown ( maybe bc. they are training enters )
|
||||
|
||||
class ProseMirrorView {
|
||||
constructor(target, content) {
|
||||
let typingTimer;
|
||||
let localDraft = localStorage.getItem(window.draftKey);
|
||||
if (localDraft != null) {
|
||||
content = localDraft;
|
||||
}
|
||||
if (content.indexOf("# ") === 0) {
|
||||
let eol = content.indexOf("\n");
|
||||
let title = content.substring("# ".length, eol);
|
||||
content = content.substring(eol + "\n\n".length);
|
||||
$title.value = title;
|
||||
}
|
||||
|
||||
const doc = writeAsMarkdownParser.parse(
|
||||
// Replace all "solo" \n's with \\\n for correct markdown parsing
|
||||
// Can't use lookahead or lookbehind because it's not supported on Safari
|
||||
content.replace(/([^]{0,1})(\n)([^]{0,1})/g, (match, p1, p2, p3) => {
|
||||
return p1 !== "\n" && p3 !== "\n" ? p1 + "\\\n" + p3 : match;
|
||||
})
|
||||
);
|
||||
|
||||
this.view = new EditorView(target, {
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
plugins: [
|
||||
keymap({
|
||||
"Mod-Enter": () => {
|
||||
document.getElementById("publish").click();
|
||||
return true;
|
||||
},
|
||||
"Mod-k": () => {
|
||||
const linkButton = document.querySelector(
|
||||
".ProseMirror-icon[title='Add or remove link']"
|
||||
);
|
||||
linkButton.dispatchEvent(new Event("mousedown"));
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
...exampleSetup({
|
||||
schema: writeFreelySchema,
|
||||
menuContent: getMenu(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
dispatchTransaction(transaction) {
|
||||
let newState = this.state.apply(transaction);
|
||||
const newContent = writeAsMarkdownSerializer
|
||||
.serialize(newState.doc)
|
||||
// Replace all \\\ns ( not followed by a \n ) with \n
|
||||
.replace(/(\\\n)(\n{0,1})/g, (match, p1, p2) =>
|
||||
p2 !== "\n" ? "\n" + p2 : match
|
||||
);
|
||||
$content.value = newContent;
|
||||
let draft = "";
|
||||
if ($title.value != null && $title.value !== "") {
|
||||
draft = "# " + $title.value + "\n\n";
|
||||
}
|
||||
draft += newContent;
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
this.updateState(newState);
|
||||
},
|
||||
});
|
||||
// Editor is focused to the last position. This is a workaround for a bug:
|
||||
// 1. 1 type something in an existing entry
|
||||
// 2. reload - works fine, the draft is reloaded
|
||||
// 3. reload again - the draft is somehow removed from localStorage and the original content is loaded
|
||||
// When the editor is focused the content is re-saved to localStorage
|
||||
|
||||
// This is also useful for editing, so it's not a bad thing even
|
||||
const lastPosition = this.view.state.doc.content.size;
|
||||
const selection = TextSelection.create(this.view.state.doc, lastPosition);
|
||||
this.view.dispatch(this.view.state.tr.setSelection(selection));
|
||||
this.view.focus();
|
||||
}
|
||||
|
||||
get content() {
|
||||
return defaultMarkdownSerializer.serialize(this.view.state.doc);
|
||||
}
|
||||
focus() {
|
||||
this.view.focus();
|
||||
}
|
||||
destroy() {
|
||||
this.view.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
let place = document.querySelector("#editor");
|
||||
let view = new ProseMirrorView(place, $content.value);
|
21
prose/schema.js
Normal file
21
prose/schema.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { schema } from "prosemirror-markdown";
|
||||
import { Schema } from "prosemirror-model";
|
||||
|
||||
export const writeFreelySchema = new Schema({
|
||||
nodes: schema.spec.nodes
|
||||
.remove("blockquote")
|
||||
.remove("horizontal_rule")
|
||||
.addToEnd("readmore", {
|
||||
inline: false,
|
||||
content: "",
|
||||
group: "block",
|
||||
draggable: true,
|
||||
toDOM: (node) => [
|
||||
"div",
|
||||
{ class: "editorreadmore" },
|
||||
"Read more...",
|
||||
],
|
||||
parseDOM: [{ tag: "div.editorreadmore" }],
|
||||
}),
|
||||
marks: schema.spec.marks,
|
||||
});
|
25
prose/webpack.config.js
Normal file
25
prose/webpack.config.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
entry: __dirname + '/prose.js'
|
||||
},
|
||||
output: {
|
||||
filename: 'prose.bundle.js',
|
||||
path: path.resolve('..', 'static', 'js'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(nodue_modules|bower_components)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -207,7 +207,6 @@ func RouteCollections(handler *Handler, r *mux.Router) {
|
|||
r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader))
|
||||
r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
|
||||
r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader))
|
||||
r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
|
||||
r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap))
|
||||
r.HandleFunc("/feed/", handler.AllReader(ViewFeed))
|
||||
r.HandleFunc("/{slug}", handler.CollectionPostOrStatic)
|
||||
|
|
|
@ -110,12 +110,24 @@ Element.prototype.show = function() {
|
|||
|
||||
|
||||
var H = {
|
||||
getQEl: function(elementQuery) {
|
||||
return new Element(document.querySelector(elementQuery));
|
||||
},
|
||||
getEl: function(elementId) {
|
||||
return new Element(document.getElementById(elementId));
|
||||
},
|
||||
save: function($el, key) {
|
||||
localStorage.setItem(key, $el.el.value);
|
||||
},
|
||||
saveClassic: function($titleEl, $el, key) {
|
||||
var out = "";
|
||||
var title = $titleEl.el.value;
|
||||
if (title !== "") {
|
||||
out = "# "+title+"\n\n";
|
||||
}
|
||||
out += $el.el.value;
|
||||
localStorage.setItem(key, out);
|
||||
},
|
||||
load: function($el, key, onlyLoadPopulated, postUpdated) {
|
||||
var val = localStorage.getItem(key);
|
||||
if (onlyLoadPopulated && val == null) {
|
||||
|
@ -138,6 +150,20 @@ var H = {
|
|||
}
|
||||
return true;
|
||||
},
|
||||
loadClassic: function($titleEl, $el, key, onlyLoadPopulated) {
|
||||
var val = localStorage.getItem(key);
|
||||
if (onlyLoadPopulated && val == null) {
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
if (val.indexOf("# ") === 0) {
|
||||
var eol = val.indexOf("\n");
|
||||
title = val.substring("# ".length, eol);
|
||||
val = val.substring(eol+"\n\n".length);
|
||||
$titleEl.el.value = title;
|
||||
}
|
||||
$el.el.value = val;
|
||||
},
|
||||
set: function(key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
|
|
2
static/js/prose.bundle.js
Normal file
2
static/js/prose.bundle.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -29,6 +29,7 @@
|
|||
<meta property="og:updated_time" content="{{.Created8601}}" />
|
||||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
||||
<meta property="article:published_time" content="{{.Created8601}}">
|
||||
{{template "collection-meta" .}}
|
||||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
|
||||
<style type="text/css">
|
||||
body footer {
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<meta property="og:url" content="{{.CanonicalURL}}" />
|
||||
<meta property="og:description" content="{{.Description}}" />
|
||||
<meta property="og:image" content="{{.AvatarURL}}">
|
||||
{{template "collection-meta" .}}
|
||||
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}}
|
||||
<style type="text/css">
|
||||
body#collection header {
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
||||
<meta property="article:published_time" content="{{.Created8601}}">
|
||||
{{ end }}
|
||||
{{template "collection-meta" .}}
|
||||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
|
||||
|
||||
{{if .Collection.RenderMathJax}}
|
||||
|
@ -62,7 +63,7 @@
|
|||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name{{if $.Collection.Format.ShowDates}} dated{{end}}">{{.FormattedDisplayTitle}}</h2>{{end}}{{if and $.Collection.Format.ShowDates (not .IsPinned) .IsFound}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
|
||||
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name{{if and $.Collection.Format.ShowDates (not .IsPinned)}} dated{{end}}">{{.FormattedDisplayTitle}}</h2>{{end}}{{if and $.Collection.Format.ShowDates (not .IsPinned) .IsFound}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
|
||||
|
||||
{{ if .Collection.ShowFooterBranding }}
|
||||
<footer dir="ltr"><hr><nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav></footer>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{.CanonicalURL}}tag:{{.Tag}}" />
|
||||
<meta property="og:image" content="{{.Collection.AvatarURL}}">
|
||||
{{template "collection-meta" .}}
|
||||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
|
||||
|
||||
{{if .Collection.RenderMathJax}}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<meta property="og:url" content="{{.CanonicalURL}}" />
|
||||
<meta property="og:description" content="{{.Description}}" />
|
||||
<meta property="og:image" content="{{.AvatarURL}}">
|
||||
{{template "collection-meta" .}}
|
||||
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}}
|
||||
|
||||
{{if .RenderMathJax}}
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
<!-- Miscelaneous render related template parts we use multiple times -->
|
||||
{{define "collection-meta"}}
|
||||
{{if .Monetization -}}
|
||||
<meta name="monetization" content="{{.Monetization}}" />
|
||||
{{- end}}
|
||||
{{end}}
|
||||
|
||||
{{define "highlighting"}}
|
||||
<script>
|
||||
// TODO: this feels more like a mutation observer
|
||||
|
|
|
@ -136,6 +136,13 @@ select {
|
|||
</label></div>
|
||||
<div><input type="checkbox" name="public_stats" id="public_stats" {{if .Config.PublicStats}}checked="checked"{{end}} /></div>
|
||||
</div>
|
||||
<div class="features row">
|
||||
<div><label for="monetization">
|
||||
Monetization
|
||||
<p>Enable blogs on this site to receive micro­pay­ments from readers via <a target="wm" href="https://webmonetization.org/">Web Monetization</a>.</p>
|
||||
</label></div>
|
||||
<div><input type="checkbox" name="monetization" id="monetization" {{if .Config.Monetization}}checked="checked"{{end}} /></div>
|
||||
</div>
|
||||
<div class="features row">
|
||||
<div><label for="min_username_len">
|
||||
Minimum Username Length
|
||||
|
|
|
@ -151,6 +151,16 @@ textarea.section.norm {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Monetization}}
|
||||
<div class="option">
|
||||
<h2>Web Monetization</h2>
|
||||
<div class="section">
|
||||
<p class="explain">Web Monetization enables you to receive micropayments from readers that have a <a href="https://coil.com">Coil membership</a>. Add your payment pointer to enable Web Monetization on your blog.</p>
|
||||
<input type="text" name="monetization_pointer" style="width:100%" value="{{.MonetizationPointer}}" placeholder="$wallet.example.com/alice" />
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="option" style="text-align: center; margin-top: 4em;">
|
||||
<input type="submit" id="save-changes" value="Save changes" />
|
||||
<p><a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">View Blog</a></p>
|
||||
|
@ -251,12 +261,15 @@ var $normalHandleEnv = document.getElementById('normal-handle-env');
|
|||
var opt = {
|
||||
showLineNumbers: false,
|
||||
showPrintMargin: 0,
|
||||
minLines: 10,
|
||||
maxLines: 40,
|
||||
};
|
||||
var theme = "ace/theme/chrome";
|
||||
var cssEditor = ace.edit("css-editor");
|
||||
cssEditor.setTheme(theme);
|
||||
cssEditor.session.setMode("ace/mode/css");
|
||||
cssEditor.setOptions(opt);
|
||||
cssEditor.resize(true);
|
||||
</script>
|
||||
|
||||
{{template "footer" .}}
|
||||
|
|
400
templates/wysiwyg.tmpl
Normal file
400
templates/wysiwyg.tmpl
Normal file
|
@ -0,0 +1,400 @@
|
|||
{{define "pad"}}<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<title>{{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/prose.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<meta name="google" value="notranslate">
|
||||
</head>
|
||||
<body id="pad" class="light classic">
|
||||
|
||||
<div id="overlay"></div>
|
||||
|
||||
<!-- <div style="text-align: center"> -->
|
||||
<!-- <label style="border-right: 1px solid silver"> -->
|
||||
<!-- Markdown <input type=radio name=inputformat value=markdown checked> </label> -->
|
||||
<!-- <label> <input type=radio name=inputformat value=prosemirror> WYSIWYM</label> -->
|
||||
<!-- </div> -->
|
||||
<input type="text" id="title" name="title" placeholder="Title..." {{if .Post.Title}}value="{{.Post.Title}}"{{end}} autofocus />
|
||||
<div id="editor" style="margin-bottom: 0"></div>
|
||||
|
||||
<div style="display: none"><textarea id="content"{{if .Post.Content }} value={{.Post.Content}}>{{.Post.Content}}{{else}}>{{end}}</textarea></div>
|
||||
|
||||
<header id="tools">
|
||||
<div id="clip">
|
||||
{{if not .SingleUser}}<h1><a href="/me/c/" title="View blogs"><img class="ic-24dp" src="/img/ic_blogs_dark@2x.png" /></a></h1>{{end}}
|
||||
<nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul>
|
||||
{{if .Editing}}<li>{{if .EditCollection}}<a href="{{.EditCollection.CanonicalURL}}">{{.EditCollection.Title}}</a>{{else}}<a>Draft</a>{{end}}</li>
|
||||
{{else}}<li><a id="publish-to"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
||||
<ul>
|
||||
<li class="menu-heading">Publish to...</li>
|
||||
{{if .Blogs}}{{range $idx, $el := .Blogs}}
|
||||
<li class="target{{if eq $idx 0}} selected{{end}}" id="blog-{{$el.Alias}}"><a href="#{{$el.Alias}}"><i class="material-icons md-18">public</i> {{if $el.Title}}{{$el.Title}}{{else}}{{$el.Alias}}{{end}}</a></li>
|
||||
{{end}}{{end}}
|
||||
<li class="target" id="blog-anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
|
||||
<li id="user-separator" class="separator"><hr /></li>
|
||||
{{ if .SingleUser }}
|
||||
<li><a href="/"><i class="material-icons md-18">launch</i> View Blog</a></li>
|
||||
<li><a href="/me/c/{{.Username}}"><i class="material-icons md-18">palette</i> Customize</a></li>
|
||||
<li><a href="/me/c/{{.Username}}/stats"><i class="material-icons md-18">trending_up</i> Stats</a></li>
|
||||
{{ else }}
|
||||
<li><a href="/me/c/"><i class="material-icons md-18">library_books</i> View Blogs</a></li>
|
||||
{{ end }}
|
||||
<li><a href="/me/posts/"><i class="material-icons md-18">view_list</i> View Drafts</a></li>
|
||||
<li><a href="/me/logout"><i class="material-icons md-18">power_settings_new</i> Log out</a></li>
|
||||
</ul>
|
||||
</li>{{end}}
|
||||
</ul></nav>
|
||||
<nav id="font-picker" class="if-room room-3 hidden" style="margin-left:-1em"><ul>
|
||||
<li><a href="#" id="" onclick="return false"><img class="ic-24dp" src="/img/ic_font_dark@2x.png" /> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
||||
<ul style="text-align: center">
|
||||
<li class="menu-heading">Font</li>
|
||||
<li class="selected"><a class="font norm" href="#norm">Serif</a></li>
|
||||
<li><a class="font sans" href="#sans">Sans-serif</a></li>
|
||||
<li><a class="font wrap" href="#wrap">Monospace</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul></nav>
|
||||
<span id="wc" class="hidden if-room room-4">0 words</span>
|
||||
</div>
|
||||
<noscript style="margin-left: 2em;"><strong>NOTE</strong>: for now, you'll need Javascript enabled to post.</noscript>
|
||||
<div id="belt">
|
||||
{{if .Editing}}<div class="tool hidden if-room"><a href="{{if .EditCollection}}{{.EditCollection.CanonicalURL}}{{.Post.Slug}}/edit/meta{{else}}/{{if .SingleUser}}d/{{end}}{{.Post.Id}}/meta{{end}}" title="Edit post metadata" id="edit-meta"><img class="ic-24dp" src="/img/ic_info_dark@2x.png" /></a></div>{{end}}
|
||||
<div class="tool hidden if-room room-2"><a href="#theme" title="Toggle theme" id="toggle-theme"><img class="ic-24dp" src="/img/ic_brightness_dark@2x.png" /></a></div>
|
||||
<div class="tool if-room room-1"><a href="{{if not .User}}/pad/posts{{else}}/me/posts/{{end}}" title="View posts" id="view-posts"><img class="ic-24dp" src="/img/ic_list_dark@2x.png" /></a></div>
|
||||
<div class="tool"><a href="#publish" title="Publish" id="publish"><img class="ic-24dp" src="/img/ic_send_dark@2x.png" /></a></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script src="/js/h.js"></script>
|
||||
<script>
|
||||
function toggleTheme() {
|
||||
var btns = Array.prototype.slice.call(document.getElementById('tools').querySelectorAll('a img'));
|
||||
var newTheme = '';
|
||||
if (document.body.classList.contains('light')) {
|
||||
newTheme = 'dark';
|
||||
document.body.className = document.body.className.replace(/(?:^|\s)light(?!\S)/g, newTheme);
|
||||
for (var i=0; i<btns.length; i++) {
|
||||
btns[i].src = btns[i].src.replace('_dark@2x.png', '@2x.png');
|
||||
}
|
||||
} else {
|
||||
TextnewTheme = 'light';
|
||||
document.body.className = document.body.className.replace(/(?:^|\s)dark(?!\S)/g, newTheme);
|
||||
for (var i=0; i<btns.length; i++) {
|
||||
btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png');
|
||||
}
|
||||
}
|
||||
H.set('padTheme', newTheme);
|
||||
}
|
||||
if (H.get('padTheme', 'light') != 'light') {
|
||||
toggleTheme();
|
||||
}
|
||||
var $title = H.getEl('title');
|
||||
var $writer = H.getQEl('div.ProseMirror');
|
||||
var $content = H.getEl('content');
|
||||
var $btnPublish = H.getEl('publish');
|
||||
var $wc = H.getEl("wc");
|
||||
var updateWordCount = function() {
|
||||
var words = 0;
|
||||
var val = $content.el.value.trim();
|
||||
if (val != '') {
|
||||
words = $content.el.value.trim().replace(/\s+/gi, ' ').split(' ').length;
|
||||
}
|
||||
val = $title.el.value.trim();
|
||||
if (val != '') {
|
||||
words += $title.el.value.trim().replace(/\s+/gi, ' ').split(' ').length;
|
||||
}
|
||||
$wc.el.innerText = words + " word" + (words != 1 ? "s" : "");
|
||||
};
|
||||
var setButtonStates = function() {
|
||||
if (!canPublish) {
|
||||
$btnPublish.el.className = 'disabled';
|
||||
return;
|
||||
}
|
||||
if ($content.el.value.length === 0 || (draftDoc != 'lastDoc' && $content.el.value == origDoc)) {
|
||||
$btnPublish.el.className = 'disabled';
|
||||
} else {
|
||||
$btnPublish.el.className = '';
|
||||
}
|
||||
};
|
||||
{{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}';
|
||||
var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}}
|
||||
|
||||
// ProseMirror editor
|
||||
window.draftKey = draftDoc;
|
||||
|
||||
// H.loadClassic($title, $writer, draftDoc, true);
|
||||
updateWordCount();
|
||||
|
||||
var typingTimer;
|
||||
var doneTypingInterval = 200;
|
||||
|
||||
var posts;
|
||||
{{if and .Post.Id (not .Post.Slug)}}
|
||||
var token = null;
|
||||
var curPostIdx;
|
||||
posts = JSON.parse(H.get('posts', '[]'));
|
||||
for (var i=0; i<posts.length; i++) {
|
||||
if (posts[i].id == "{{.Post.Id}}") {
|
||||
token = posts[i].token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var canPublish = token != null;
|
||||
{{else}}var canPublish = true;{{end}}
|
||||
var publishing = false;
|
||||
var justPublished = false;
|
||||
var silenced = {{.Silenced}};
|
||||
var publish = function(title, content, font) {
|
||||
if (silenced === true) {
|
||||
alert("Your account is silenced, so you can't publish or update posts.");
|
||||
return;
|
||||
}
|
||||
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
|
||||
if (!token) {
|
||||
alert("You don't have permission to update this post.");
|
||||
return;
|
||||
}
|
||||
if ($btnPublish.el.className == 'disabled') {
|
||||
return;
|
||||
}
|
||||
{{end}}
|
||||
$btnPublish.el.children[0].textContent = 'more_horiz';
|
||||
publishing = true;
|
||||
var xpostTarg = H.get('crosspostTarget', '[]');
|
||||
|
||||
var http = new XMLHttpRequest();
|
||||
var lang = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
|
||||
lang = lang.substring(0, 2);
|
||||
var post = H.getTitleStrict(content);
|
||||
|
||||
var params = {
|
||||
body: post.content,
|
||||
title: title,
|
||||
font: font,
|
||||
lang: lang
|
||||
};
|
||||
{{ if .Post.Slug }}
|
||||
var url = "/api/collections/{{.EditCollection.Alias}}/posts/{{.Post.Id}}";
|
||||
{{ else if .Post.Id }}
|
||||
var url = "/api/posts/{{.Post.Id}}";
|
||||
if (typeof token === 'undefined' || !token) {
|
||||
token = "";
|
||||
}
|
||||
params.token = token;
|
||||
{{ else }}
|
||||
var url = "/api/posts";
|
||||
var postTarget = H.get('postTarget', 'anonymous');
|
||||
if (postTarget != 'anonymous') {
|
||||
url = "/api/collections/" + postTarget + "/posts";
|
||||
}
|
||||
params.crosspost = JSON.parse(xpostTarg);
|
||||
{{ end }}
|
||||
|
||||
http.open("POST", url, true);
|
||||
|
||||
// Send the proper header information along with the request
|
||||
http.setRequestHeader("Content-type", "application/json");
|
||||
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
publishing = false;
|
||||
if (http.status == 200 || http.status == 201) {
|
||||
data = JSON.parse(http.responseText);
|
||||
id = data.data.id;
|
||||
nextURL = '{{if .SingleUser}}/d{{end}}/'+id;
|
||||
|
||||
{{ if not .Post.Id }}
|
||||
// Post created
|
||||
if (postTarget != 'anonymous') {
|
||||
nextURL = {{if not .SingleUser}}'/'+postTarget+{{end}}'/'+data.data.slug;
|
||||
}
|
||||
editToken = data.data.token;
|
||||
|
||||
{{ if not .User }}if (postTarget == 'anonymous') {
|
||||
// Save the data
|
||||
var posts = JSON.parse(H.get('posts', '[]'));
|
||||
|
||||
{{if .Post.Id}}var newPost = H.createPost("{{.Post.Id}}", token, content);
|
||||
for (var i=0; i<posts.length; i++) {
|
||||
if (posts[i].id == "{{.Post.Id}}") {
|
||||
posts[i].title = newPost.title;
|
||||
posts[i].summary = newPost.summary;
|
||||
break;
|
||||
}
|
||||
}
|
||||
nextURL = "/pad/posts";{{else}}posts.push(H.createPost(id, editToken, content));{{end}}
|
||||
|
||||
H.set('posts', JSON.stringify(posts));
|
||||
}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
justPublished = true;
|
||||
if (draftDoc != 'lastDoc') {
|
||||
H.remove(draftDoc);
|
||||
{{if .Editing}}H.remove('draft{{.Post.Id}}font');{{end}}
|
||||
} else {
|
||||
H.set(draftDoc, '');
|
||||
}
|
||||
|
||||
{{if .EditCollection}}
|
||||
window.location = '{{.EditCollection.CanonicalURL}}{{.Post.Slug}}';
|
||||
{{else}}
|
||||
window.location = nextURL;
|
||||
{{end}}
|
||||
} else {
|
||||
$btnPublish.el.children[0].textContent = 'send';
|
||||
alert("Failed to post. Please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
http.send(JSON.stringify(params));
|
||||
};
|
||||
|
||||
setButtonStates();
|
||||
$title.on('keydown', function(e) {
|
||||
if (e.keyCode == 13) {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
$btnPublish.el.click();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
$writer.el.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
/*
|
||||
$writer.on('keyup input', function() {
|
||||
setButtonStates();
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
}, false);
|
||||
$writer.on('keydown', function(e) {
|
||||
clearTimeout(typingTimer);
|
||||
if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) {
|
||||
$btnPublish.el.click();
|
||||
}
|
||||
});
|
||||
*/
|
||||
$btnPublish.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (!publishing && $content.el.value) {
|
||||
var title = $title.el.value;
|
||||
var content = $content.el.value;
|
||||
publish(title, content, selectedFont);
|
||||
}
|
||||
});
|
||||
|
||||
H.getEl('toggle-theme').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var newTheme = 'light';
|
||||
if (document.body.className == 'light') {
|
||||
newTheme = 'dark';
|
||||
}
|
||||
toggleTheme();
|
||||
});
|
||||
|
||||
var targets = document.querySelectorAll('#target li.target a');
|
||||
for (var i=0; i<targets.length; i++) {
|
||||
targets[i].addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var targetName = this.href.substring(this.href.indexOf('#')+1);
|
||||
H.set('postTarget', targetName);
|
||||
|
||||
document.querySelector('#target li.target.selected').classList.remove('selected');
|
||||
this.parentElement.classList.add('selected');
|
||||
var newText = this.innerText.split(' ');
|
||||
newText.shift();
|
||||
document.getElementById('target-name').innerText = newText.join(' ');
|
||||
});
|
||||
}
|
||||
var postTarget = H.get('postTarget', '{{if .Blogs}}{{$blog := index .Blogs 0}}{{$blog.Alias}}{{else}}anonymous{{end}}');
|
||||
if (location.hash != '') {
|
||||
postTarget = location.hash.substring(1);
|
||||
// TODO: pushState to /pad (or whatever the URL is) so we live on a clean URL
|
||||
location.hash = '';
|
||||
}
|
||||
var pte = document.querySelector('#target li.target#blog-'+postTarget+' a');
|
||||
if (pte != null) {
|
||||
pte.click();
|
||||
} else {
|
||||
postTarget = 'anonymous';
|
||||
H.set('postTarget', postTarget);
|
||||
}
|
||||
|
||||
var sansLoaded = false;
|
||||
WebFontConfig = {
|
||||
custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] }
|
||||
};
|
||||
var loadSans = function() {
|
||||
if (sansLoaded) return;
|
||||
sansLoaded = true;
|
||||
WebFontConfig.custom.families.push('Open+Sans:400,700:latin');
|
||||
try {
|
||||
(function() {
|
||||
var wf=document.createElement('script');
|
||||
wf.src = '/js/webfont.js';
|
||||
wf.type='text/javascript';
|
||||
wf.async='true';
|
||||
var s=document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(wf, s);
|
||||
})();
|
||||
} catch (e) {}
|
||||
};
|
||||
var fonts = document.querySelectorAll('nav#font-picker a.font');
|
||||
for (var i=0; i<fonts.length; i++) {
|
||||
fonts[i].addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
selectedFont = this.href.substring(this.href.indexOf('#')+1);
|
||||
// TODO: don't change classes on the editor window
|
||||
//$title.el.className = selectedFont;
|
||||
//$writer.el.className = selectedFont;
|
||||
document.querySelector('nav#font-picker li.selected').classList.remove('selected');
|
||||
this.parentElement.classList.add('selected');
|
||||
H.set('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', selectedFont);
|
||||
if (selectedFont == 'sans') {
|
||||
loadSans();
|
||||
}
|
||||
});
|
||||
}
|
||||
var selectedFont = H.get('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', '{{.Post.Font}}');
|
||||
var sfe = document.querySelector('nav#font-picker a.font.'+selectedFont);
|
||||
if (sfe != null) {
|
||||
sfe.click();
|
||||
}
|
||||
|
||||
var doneTyping = function() {
|
||||
if (draftDoc == 'lastDoc' || $content.el.value != origDoc) {
|
||||
H.saveClassic($title, $content, draftDoc);
|
||||
updateWordCount();
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (draftDoc != 'lastDoc' && $content.el.value == origDoc) {
|
||||
H.remove(draftDoc);
|
||||
} else if (!justPublished) {
|
||||
doneTyping();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
(function() {
|
||||
var wf=document.createElement('script');
|
||||
wf.src = '/js/webfont.js';
|
||||
wf.type='text/javascript';
|
||||
wf.async='true';
|
||||
var s=document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(wf, s);
|
||||
})();
|
||||
} catch (e) {
|
||||
// whatevs
|
||||
}
|
||||
</script>
|
||||
<script src="/js/prose.bundle.js"></script>
|
||||
<link href="/css/icons.css" rel="stylesheet">
|
||||
</body>
|
||||
</html>{{end}}
|
Loading…
Reference in a new issue