commit
5d302bd792
@ -0,0 +1,5 @@
|
||||
data*
|
||||
build*
|
||||
node_modules
|
||||
Dockerfile
|
||||
src/python
|
@ -0,0 +1 @@
|
||||
lib/* linguist-vendored
|
@ -0,0 +1,29 @@
|
||||
name: Test
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.13"
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.9"
|
||||
architecture: x64
|
||||
|
||||
- name: Install project dependencies
|
||||
run: make install-deps
|
||||
|
||||
- name: Build project
|
||||
run: make
|
||||
|
||||
- name: Run tests
|
||||
run: make run & make large-tests
|
@ -0,0 +1,25 @@
|
||||
*.[568vq]
|
||||
*.a
|
||||
*.bin
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
*.exe
|
||||
*.o
|
||||
*.prof
|
||||
*.so
|
||||
*.test
|
||||
[568vq].out
|
||||
_cgo_defun.c
|
||||
_cgo_export.*
|
||||
_cgo_gotypes.go
|
||||
_obj
|
||||
_test
|
||||
_testmain.go
|
||||
build
|
||||
data*
|
||||
dist
|
||||
node_modules
|
||||
.idea
|
||||
*.swp
|
||||
.py*
|
||||
__pycache__
|
@ -0,0 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.0] - 2020-02-07
|
||||
### Added
|
||||
- Add support for checkmarks and nested lists (similar to GitHub)
|
||||
- Allow user to add formulas with [MathJax](https://www.mathjax.org/)
|
||||
- Allow user to create diagrams with [Mermaid](https://mermaid-js.github.io/mermaid/#/)
|
||||
- When editing, show raw content and preview side-by-side
|
||||
|
||||
### Changed
|
||||
- Better organize data directory
|
||||
- Simplify navigation bar
|
||||
- Switch Markdown engine from [blackfriday](https://github.com/russross/blackfriday) to [goldmark](https://github.com/yuin/goldmark)
|
||||
- Always sanitize Markdown output with [bluemonday](https://github.com/microcosm-cc/bluemonday)
|
||||
- Use random strings for URLs instead of animal names
|
||||
- Page publishing now simply generates a read-only URL
|
||||
|
||||
### Removed
|
||||
- Remove built-in TLS support (use a reverse proxy instead)
|
||||
- Remove cowyo lists
|
||||
- Remove custom CSS
|
||||
- Remove multi-website wikis
|
||||
- Remove page encryption
|
||||
- Remove page locking
|
||||
- Remove self-destructing pages
|
||||
- Remove sitemap.xml
|
@ -0,0 +1,12 @@
|
||||
FROM golang:1.12-alpine as builder
|
||||
RUN apk add --no-cache git make rsync nodejs npm
|
||||
WORKDIR /go/notes
|
||||
COPY . .
|
||||
RUN make install-deps all
|
||||
|
||||
FROM alpine:latest
|
||||
VOLUME /data
|
||||
EXPOSE 8050
|
||||
COPY --from=builder /go/notes/build/ /
|
||||
ENTRYPOINT ["/notes"]
|
||||
CMD ["--data","/data","--allow-file-uploads","--max-upload-mb","10","--host","0.0.0.0"]
|
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2020-2021 Alinson S. Xavier <git@axavier.org>
|
||||
Copyright (c) 2016-2018 Zack Scholl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,71 @@
|
||||
VERSION=0.1.0
|
||||
LDFLAGS=-ldflags "-X main.version=${VERSION}"
|
||||
|
||||
ROLLUP := node_modules/.bin/rollup
|
||||
|
||||
TEMPLATES_IN := $(wildcard src/templates/*.tmpl)
|
||||
TEMPLATES_OUT := $(patsubst src/%,build/%,$(TEMPLATES_IN))
|
||||
CSS_IN := $(wildcard src/css/*.css)
|
||||
CSS_OUT := $(patsubst src/css/%,build/static/%,$(CSS_IN))
|
||||
JS_IN := $(wildcard src/js/*)
|
||||
JS_OUT := build/static/notes.bundle.js
|
||||
GO_IN := $(wildcard src/go/**/*.go)
|
||||
GO_OUT := build/notes
|
||||
OUTPUT_FILES := $(GO_OUT) $(JS_OUT) $(TEMPLATES_OUT) $(CSS_OUT)
|
||||
|
||||
all: $(OUTPUT_FILES)
|
||||
@rsync -avP lib/ build/static/lib/
|
||||
@rsync -avP node_modules/\@fontsource/roboto/files/roboto-all* build/static/lib/
|
||||
@rsync -avP node_modules/mathjax/es5 build/static/lib/mathjax
|
||||
@rsync -avP node_modules/mermaid/dist/mermaid.min.js build/static/lib/
|
||||
@rsync -avP node_modules/jquery/dist/jquery.min.js build/static/lib/
|
||||
|
||||
$(GO_OUT): $(GO_IN)
|
||||
cd src/go && go build ${LDFLAGS} -o ../../build/notes
|
||||
|
||||
$(JS_OUT): $(JS_IN)
|
||||
$(ROLLUP) $(JS_IN) --file $(JS_OUT) --format iife
|
||||
|
||||
build/static/%.css: src/css/%.css
|
||||
@mkdir -p `dirname $@`
|
||||
cp $< $@
|
||||
|
||||
build/templates/%.tmpl: src/templates/%.tmpl
|
||||
@mkdir -p `dirname $@`
|
||||
cp $< $@
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rfv build
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build:
|
||||
docker build . --tag isoron/notes:$(VERSION)
|
||||
docker tag isoron/notes:$(VERSION) isoron/notes:latest
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push:
|
||||
docker push isoron/notes:$(VERSION)
|
||||
docker push isoron/notes:latest
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run:
|
||||
docker run \
|
||||
--userns host \
|
||||
-it \
|
||||
--volume `pwd`/data:/data \
|
||||
--publish 8050:8050 \
|
||||
isoron/notes:$(VERSION)
|
||||
|
||||
.PHONY: install-deps
|
||||
install-deps:
|
||||
npm install
|
||||
pip install -r src/python/requirements.txt
|
||||
|
||||
.PHONY: run
|
||||
run:
|
||||
cd build && ./notes
|
||||
|
||||
.PHONY: large-tests
|
||||
large-tests:
|
||||
pytest
|
@ -0,0 +1,45 @@
|
||||
# Notes
|
||||
|
||||
**Notes** is minimalist and collaborative wiki webserver with LaTeX support. Originally based on [cowyo](https://github.com/schollz/cowyo).
|
||||
|
||||
## Features
|
||||
|
||||
- Simplified user interface
|
||||
- Support for checkmarks and nested lists (similar to GitHub)
|
||||
- Support for formulas with [MathJax](https://www.mathjax.org/)
|
||||
- Support for diagrams with [Mermaid](https://mermaid-js.github.io/mermaid/#/)
|
||||
- Live Markdown preview while editing
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--publish 8050:8050 \
|
||||
--volume $(pwd)/data:/data \
|
||||
isoron/notes
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
```text
|
||||
Copyright (c) 2020-2021 Alinson S. Xavier <git@axavier.org>
|
||||
Copyright (c) 2016-2018 Zack Scholl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
@ -0,0 +1,11 @@
|
||||
/*!
|
||||
Pure v0.6.2
|
||||
Copyright 2013 Yahoo!
|
||||
Licensed under the BSD License.
|
||||
https://github.com/yahoo/pure/blob/master/LICENSE.md
|
||||
*/
|
||||
/*!
|
||||
normalize.css v^3.0 | MIT License | git.io/normalize
|
||||
Copyright (c) Nicolas Gallagher and Jonathan Neal
|
||||
*/
|
||||
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */img,legend{border:0}legend,td,th{padding:0}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}table{border-collapse:collapse;border-spacing:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}
|
@ -0,0 +1,388 @@
|
||||
/*
|
||||
* The MIT License
|
||||
* Copyright (c) 2012 Matias Meno <m@tias.me>
|
||||
*/
|
||||
@-webkit-keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); }
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px); } }
|
||||
@-moz-keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); }
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px); } }
|
||||
@keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); }
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px); } }
|
||||
@-webkit-keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); } }
|
||||
@-moz-keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); } }
|
||||
@keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); } }
|
||||
@-webkit-keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1); }
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); } }
|
||||
@-moz-keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1); }
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); } }
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1); }
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); } }
|
||||
.dropzone, .dropzone * {
|
||||
box-sizing: border-box; }
|
||||
|
||||
.dropzone {
|
||||
min-height: 150px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.3);
|
||||
background: white;
|
||||
padding: 20px 20px; }
|
||||
.dropzone.dz-clickable {
|
||||
cursor: pointer; }
|
||||
.dropzone.dz-clickable * {
|
||||
cursor: default; }
|
||||
.dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * {
|
||||
cursor: pointer; }
|
||||
.dropzone.dz-started .dz-message {
|
||||
display: none; }
|
||||
.dropzone.dz-drag-hover {
|
||||
border-style: solid; }
|
||||
.dropzone.dz-drag-hover .dz-message {
|
||||
opacity: 0.5; }
|
||||
.dropzone .dz-message {
|
||||
text-align: center;
|
||||
margin: 2em 0; }
|
||||
.dropzone .dz-preview {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 16px;
|
||||
min-height: 100px; }
|
||||
.dropzone .dz-preview:hover {
|
||||
z-index: 1000; }
|
||||
.dropzone .dz-preview:hover .dz-details {
|
||||
opacity: 1; }
|
||||
.dropzone .dz-preview.dz-file-preview .dz-image {
|
||||
border-radius: 20px;
|
||||
background: #999;
|
||||
background: linear-gradient(to bottom, #eee, #ddd); }
|
||||
.dropzone .dz-preview.dz-file-preview .dz-details {
|
||||
opacity: 1; }
|
||||
.dropzone .dz-preview.dz-image-preview {
|
||||
background: white; }
|
||||
.dropzone .dz-preview.dz-image-preview .dz-details {
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
-ms-transition: opacity 0.2s linear;
|
||||
-o-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear; }
|
||||
.dropzone .dz-preview .dz-remove {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
border: none; }
|
||||
.dropzone .dz-preview .dz-remove:hover {
|
||||
text-decoration: underline; }
|
||||
.dropzone .dz-preview:hover .dz-details {
|
||||
opacity: 1; }
|
||||
.dropzone .dz-preview .dz-details {
|
||||
z-index: 20;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
font-size: 13px;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 2em 1em;
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
line-height: 150%; }
|
||||
.dropzone .dz-preview .dz-details .dz-size {
|
||||
margin-bottom: 1em;
|
||||
font-size: 16px; }
|
||||
.dropzone .dz-preview .dz-details .dz-filename {
|
||||
white-space: nowrap; }
|
||||
.dropzone .dz-preview .dz-details .dz-filename:hover span {
|
||||
border: 1px solid rgba(200, 200, 200, 0.8);
|
||||
background-color: rgba(255, 255, 255, 0.8); }
|
||||
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
|
||||
border: 1px solid transparent; }
|
||||
.dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
padding: 0 0.4em;
|
||||
border-radius: 3px; }
|
||||
.dropzone .dz-preview:hover .dz-image img {
|
||||
-webkit-transform: scale(1.05, 1.05);
|
||||
-moz-transform: scale(1.05, 1.05);
|
||||
-ms-transform: scale(1.05, 1.05);
|
||||
-o-transform: scale(1.05, 1.05);
|
||||
transform: scale(1.05, 1.05);
|
||||
-webkit-filter: blur(8px);
|
||||
filter: blur(8px); }
|
||||
.dropzone .dz-preview .dz-image {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
position: relative;
|
||||
display: block;
|
||||
z-index: 10; }
|
||||
.dropzone .dz-preview .dz-image img {
|
||||
display: block; }
|
||||
.dropzone .dz-preview.dz-success .dz-success-mark {
|
||||
-webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); }
|
||||
.dropzone .dz-preview.dz-error .dz-error-mark {
|
||||
opacity: 1;
|
||||
-webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); }
|
||||
.dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
z-index: 500;
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -27px;
|
||||
margin-top: -27px; }
|
||||
.dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg {
|
||||
display: block;
|
||||
width: 54px;
|
||||
height: 54px; }
|
||||
.dropzone .dz-preview.dz-processing .dz-progress {
|
||||
opacity: 1;
|
||||
-webkit-transition: all 0.2s linear;
|
||||
-moz-transition: all 0.2s linear;
|
||||
-ms-transition: all 0.2s linear;
|
||||
-o-transition: all 0.2s linear;
|
||||
transition: all 0.2s linear; }
|
||||
.dropzone .dz-preview.dz-complete .dz-progress {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.4s ease-in;
|
||||
-moz-transition: opacity 0.4s ease-in;
|
||||
-ms-transition: opacity 0.4s ease-in;
|
||||
-o-transition: opacity 0.4s ease-in;
|
||||
transition: opacity 0.4s ease-in; }
|
||||
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
|
||||
-webkit-animation: pulse 6s ease infinite;
|
||||
-moz-animation: pulse 6s ease infinite;
|
||||
-ms-animation: pulse 6s ease infinite;
|
||||
-o-animation: pulse 6s ease infinite;
|
||||
animation: pulse 6s ease infinite; }
|
||||
.dropzone .dz-preview .dz-progress {
|
||||
opacity: 1;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
width: 80px;
|
||||
margin-left: -40px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
-webkit-transform: scale(1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden; }
|
||||
.dropzone .dz-preview .dz-progress .dz-upload {
|
||||
background: #333;
|
||||
background: linear-gradient(to bottom, #666, #444);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
-webkit-transition: width 300ms ease-in-out;
|
||||
-moz-transition: width 300ms ease-in-out;
|
||||
-ms-transition: width 300ms ease-in-out;
|
||||
-o-transition: width 300ms ease-in-out;
|
||||
transition: width 300ms ease-in-out; }
|
||||
.dropzone .dz-preview.dz-error .dz-error-message {
|
||||
display: block; }
|
||||
.dropzone .dz-preview.dz-error:hover .dz-error-message {
|
||||
opacity: 1;
|
||||
pointer-events: auto; }
|
||||
.dropzone .dz-preview .dz-error-message {
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
display: block;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.3s ease;
|
||||
-moz-transition: opacity 0.3s ease;
|
||||
-ms-transition: opacity 0.3s ease;
|
||||
-o-transition: opacity 0.3s ease;
|
||||
transition: opacity 0.3s ease;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
top: 130px;
|
||||
left: -10px;
|
||||
width: 140px;
|
||||
background: #be2626;
|
||||
background: linear-gradient(to bottom, #be2626, #a92222);
|
||||
padding: 0.5em 1.2em;
|
||||
color: white; }
|
||||
.dropzone .dz-preview .dz-error-message:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 64px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #be2626; }
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
.hljs{display:block;overflow-x:auto;padding:0.5em;background:#F0F0F0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888888}.hljs-keyword,.hljs-attribute,.hljs-selector-tag,.hljs-meta-keyword,.hljs-doctag,.hljs-name{font-weight:bold}.hljs-type,.hljs-string,.hljs-number,.hljs-selector-id,.hljs-selector-class,.hljs-quote,.hljs-template-tag,.hljs-deletion{color:#880000}.hljs-title,.hljs-section{color:#880000;font-weight:bold}.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr,.hljs-selector-pseudo{color:#BC6060}.hljs-literal{color:#78A960}.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-addition{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,7 @@
|
||||
/*!
|
||||
Pure v0.6.2
|
||||
Copyright 2013 Yahoo!
|
||||
Licensed under the BSD License.
|
||||
https://github.com/yahoo/pure/blob/master/LICENSE.md
|
||||
*/
|
||||
.pure-menu{box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^4.2.0",
|
||||
"jquery": "^1.12.4",
|
||||
"mathjax": "^3.1.2",
|
||||
"mermaid": "^8.9.0",
|
||||
"rollup": "^2.38.5"
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
src: url("/static/lib/roboto-all-400-normal.woff") format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
src: url("/static/lib/roboto-all-400-italic.woff") format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
src: url("/static/lib/roboto-all-700-normal.woff") format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
src: url("/static/lib/roboto-all-700-italic.woff") format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
src: url("/static/lib/roboto-all-500-normal.woff") format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
src: url("/static/lib/roboto-all-500-italic.woff") format('woff2');
|
||||
}
|
||||
|
||||
|
||||
.markdown-body a {
|
||||
color: #06d;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 13pt;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.markdown-body strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
body.ListPage span {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #5cb85c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.failure {
|
||||
color: #d9534f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.deleting {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ChildPageNames ul {
|
||||
margin: 0.3em 0 0 1.6em;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ChildPageNames li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ChildPageNames a {
|
||||
color: #0645ad;
|
||||
background: none;
|
||||
}
|
||||
|
||||
#menu {
|
||||
background-color: #f6f6f6;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
#menu ul {
|
||||
list-style: none;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
max-width: 55em;
|
||||
}
|
||||
|
||||
#menu li {
|
||||
display: inline-block;
|
||||
color: #666;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
|
||||
#menu a {
|
||||
padding: 0.5em 1.5em;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#menu a:hover {
|
||||
color: #06d;
|
||||
}
|
||||
|
||||
#menu .selected {
|
||||
background-color: #fff;
|
||||
padding: 0.5em 1.5em;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: 1px solid white;
|
||||
border-top: 0px solid white;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
ul.checkmark {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li.checkmark input {
|
||||
margin: 0 .2em .25em -1.6em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
li.checkmark.done {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin: 1.5em;
|
||||
}
|
||||
|
||||
.table-wrapper table {
|
||||
display: inline;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.history {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#pad {
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
width: 100%;
|
||||
height: 95%;
|
||||
}
|
||||
|
||||
#pad form, #pad textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: none;
|
||||
resize: none;
|
||||
padding: 0 2em;
|
||||
font-size: 1.0em;
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
}
|
||||
|
||||
.EditPage #rendered {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ViewPage #rendered, .HistoryPage #rendered, .ReadPage #rendered {
|
||||
max-width: 55em;
|
||||
margin: 0 auto;
|
||||
padding: 0 2%;
|
||||
}
|
||||
|
||||
@media (min-width: 75em) {
|
||||
#pad {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.EditPage #rendered {
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
height: 95%;
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
module github.com/isoron/notes
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20190528082104-30e424939505
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.4.0
|
||||
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20200921072530-4c5dae64834a
|
||||
github.com/mattn/go-isatty v0.0.8 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.3
|
||||
github.com/schollz/versionedtext v0.0.0-20180523061923-d8ce0957c254
|
||||
github.com/ugorji/go v1.1.7 // indirect
|
||||
github.com/yuin/goldmark v1.3.1
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/urfave/cli.v1 v1.20.0
|
||||
)
|
@ -0,0 +1,76 @@
|
||||
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/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
|
||||
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20190528082104-30e424939505 h1:ooJvd+PTXxaHDJBP++saAcxinwBbOW/YJR7N4/Twbfk=
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20190528082104-30e424939505/go.mod h1:2tmLQ8sVzr2XKwquGd7zNq3zB6fGyjJL+47JoxoF8yM=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8=
|
||||
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20200921072530-4c5dae64834a h1:l12N1wrrK490w3IMtrdr5GMFHOesCIA9Tv8WvUh+H/Y=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20200921072530-4c5dae64834a/go.mod h1:FdZNqN8u5ntryGYCpJHnTJLLEpjWCPvL8HGZmZFBXe4=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/microcosm-cc/bluemonday v1.0.3 h1:EjVH7OqbU219kdm8acbveoclh2zZFqPJTJw6VUlTLAQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/schollz/versionedtext v0.0.0-20180523061923-d8ce0957c254 h1:/EgihFrDLhb/x7NLm8cWB7QTquw5gatR+y/jv2gLWsY=
|
||||
github.com/schollz/versionedtext v0.0.0-20180523061923-d8ce0957c254/go.mod h1:116sjYSGDGoVSTUCdO34dA1Yg1ZGbN2jk/aYThLfK60=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.1 h1:eVwehsLsZlCJCwXyGLgg+Q4iFWE/eTIMG0e8waCmm/I=
|
||||
github.com/yuin/goldmark v1.3.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0=
|
||||
gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/isoron/notes/notes"
|
||||
"github.com/jcelliott/lumber"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var version string
|
||||
var dataDir string
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "notes"
|
||||
app.Usage = "a simple wiki"
|
||||
app.Version = version
|
||||
app.Compiled = time.Now()
|
||||
app.Action = func(c *cli.Context) error {
|
||||
dataDir = c.GlobalString("data")
|
||||
site := notes.Site{
|
||||
DataDir: dataDir,
|
||||
AllowFileUploads: c.GlobalBool("allow-file-uploads"),
|
||||
MaxUploadSizeInMB: c.GlobalUint("max-upload-mb"),
|
||||
Logger: lumber.NewConsoleLogger(lumber.TRACE),
|
||||
MaxPageContentSizeInMB: c.GlobalUint("max-document-length"),
|
||||
}
|
||||
err := site.Migrate()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
site.Run(
|
||||
c.GlobalString("host"),
|
||||
c.GlobalString("port"),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "data",
|
||||
Value: "data",
|
||||
Usage: "data folder to use",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: "0.0.0.0",
|
||||
Usage: "host to use",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "port,p",
|
||||
Value: "8050",
|
||||
Usage: "port to use",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "allow-file-uploads",
|
||||
Usage: "Enable file uploads",
|
||||
},
|
||||
cli.UintFlag{
|
||||
Name: "max-upload-mb",
|
||||
Value: 2,
|
||||
Usage: "Largest file upload (in mb) allowed",
|
||||
},
|
||||
cli.UintFlag{
|
||||
Name: "max-document-length",
|
||||
Value: 100000000,
|
||||
Usage: "Largest wiki page (in characters) allowed",
|
||||
},
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
package notes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/schollz/versionedtext"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (site *Site) Migrate() error {
|
||||
currentVersion := site.CurrentDataVersion()
|
||||
if currentVersion < 2 {
|
||||
err := site.MigrateToVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if currentVersion < 3 {
|
||||
err := site.MigrateToVersion3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) CurrentDataVersion() int {
|
||||
// If meta.json does not exist, assume version 1
|
||||
_, err := os.Stat(path.Join(site.DataDir, "meta.json"))
|
||||
if os.IsNotExist(err) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read version from meta.json
|
||||
metaFile, err := ioutil.ReadFile(path.Join(site.DataDir, "meta.json"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var meta struct {
|
||||
Version int
|
||||
}
|
||||
err = json.Unmarshal(metaFile, &meta)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return meta.Version
|
||||
}
|
||||
|
||||
func (site *Site) MigrateToVersion2() error {
|
||||
log.Println("Migrating to Version 2...")
|
||||
|
||||
// Create uploads dir
|
||||
err := os.MkdirAll(path.Join(site.DataDir, "uploads"), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create pages dir
|
||||
err = os.MkdirAll(path.Join(site.DataDir, "pages"), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete sitemap.xml if it exists
|
||||
sitemapPath := path.Join(site.DataDir, "sitemap.xml")
|
||||
_, err = os.Stat(sitemapPath)
|
||||
if !os.IsNotExist(err) {
|
||||
log.Println("Removing: sitemap.xml")
|
||||
err = os.Remove(sitemapPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse files in datadir
|
||||
fileInfos, err := ioutil.ReadDir(site.DataDir)
|
||||
for _, fileInfo := range fileInfos {
|
||||
filePath := path.Join(site.DataDir, fileInfo.Name())
|
||||
|
||||
// Process JSON files (pages)
|
||||
if strings.HasSuffix(filePath, ".json") {
|
||||
|
||||
// Read file
|
||||
oldPageFile, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var oldPage struct {
|
||||
Name string
|
||||
Text versionedtext.VersionedText
|
||||
RenderedPage string
|
||||
}
|
||||
err = json.Unmarshal(oldPageFile, &oldPage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate new JSON
|
||||
var page struct {
|
||||
Name string
|
||||
RawContent versionedtext.VersionedText
|
||||
RenderedContent string
|
||||
}
|
||||
page.Name = oldPage.Name
|
||||
page.RawContent = oldPage.Text
|
||||
page.RenderedContent = oldPage.RenderedPage
|
||||
pageJson, err := json.MarshalIndent(page, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write new file
|
||||
newFilePath := path.Join(site.DataDir, "pages", page.Name+".json")
|
||||
log.Printf("Writing: %s\n", newFilePath)
|
||||
err = ioutil.WriteFile(newFilePath, pageJson, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete old file
|
||||
log.Printf("Removing: %s\n", filePath)
|
||||
err = os.Remove(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Process uploads
|
||||
if strings.HasSuffix(filePath, ".upload") {
|
||||
newFilePath := path.Join(site.DataDir, "uploads", path.Base(filePath))
|
||||
log.Printf("Moving: %s -> %s\n", filePath, newFilePath)
|
||||
err = os.Rename(filePath, newFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new meta JSON
|
||||
var meta struct {
|
||||
Version int
|
||||
}
|
||||
meta.Version = 2
|
||||
metaJson, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write meta.json
|
||||
metaPath := path.Join(site.DataDir, "meta.json")
|
||||
log.Printf("Writing: %s\n", metaPath)
|
||||
err = ioutil.WriteFile(metaPath, metaJson, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) MigrateToVersion3() error {
|
||||
// Create publish dir
|
||||
err := os.MkdirAll(path.Join(site.DataDir, "publish"), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Traverse pages
|
||||
fileInfos, err := ioutil.ReadDir(path.Join(site.DataDir, "pages"))
|
||||
for _, fileInfo := range fileInfos {
|
||||
filePath := path.Join(site.DataDir, "pages", fileInfo.Name())
|
||||
// Read file
|
||||
oldPageFile, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var oldPage struct {
|
||||
Name string
|
||||
RawContent versionedtext.VersionedText
|
||||
RenderedContent string
|
||||
}
|
||||
err = json.Unmarshal(oldPageFile, &oldPage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate new JSON
|
||||
var page struct {
|
||||
Name string
|
||||
ReadOnlyName string
|
||||
RawContent versionedtext.VersionedText
|
||||
RenderedContent string
|
||||
}
|
||||
page.Name = oldPage.Name
|
||||
page.ReadOnlyName = randomString(32)
|
||||
page.RawContent = oldPage.RawContent
|
||||
page.RenderedContent = oldPage.RenderedContent
|
||||
pageJson, err := json.MarshalIndent(page, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write new file
|
||||
newFilePath := path.Join(site.DataDir, "pages", page.Name+".json")
|
||||
log.Printf("Writing: %s\n", filePath)
|
||||
err = ioutil.WriteFile(newFilePath, pageJson, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
err = os.Symlink(
|
||||
path.Join("../pages", page.Name+".json"),
|
||||
path.Join(site.DataDir, "publish", page.ReadOnlyName+".json"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create new meta JSON
|
||||
var meta struct {
|
||||
Version int
|
||||
}
|
||||
meta.Version = 3
|
||||
metaJson, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write meta.json
|
||||
metaPath := path.Join(site.DataDir, "meta.json")
|
||||
log.Printf("Writing: %s\n", metaPath)
|
||||
err = ioutil.WriteFile(metaPath, metaJson, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
package notes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/schollz/versionedtext"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Site *Site
|
||||
Name string
|
||||
ReadOnlyName string
|
||||
RawContent versionedtext.VersionedText
|
||||
RenderedContent string
|
||||
}
|
||||
|
||||
func (p Page) LastEditUnixTime() int64 {
|
||||
return p.RawContent.LastEditTime() / 1000000000
|
||||
}
|
||||
|
||||
func (site *Site) Load(name string) (*Page, error) {
|
||||
matched, _ := regexp.MatchString(`^[a-z0-9]*$`, name)
|
||||
if !matched || len(name) > 64 || len(name) < 3 {
|
||||
return nil, fmt.Errorf("Invalid note name: %s", name)
|
||||
}
|
||||
page := new(Page)
|
||||
page.Site = site
|
||||
page.Name = name
|
||||
page.ReadOnlyName = randomString(32)
|
||||
page.RawContent = versionedtext.NewVersionedText("")
|
||||
page.Render()
|
||||
|
||||
// Read file
|
||||
pageJson, err := ioutil.ReadFile(page.FileName())
|
||||
if os.IsNotExist(err) {
|
||||
// If page does not exist, create a new one
|
||||
err = os.Symlink(
|
||||
path.Join("../pages", page.Name + ".json"),
|
||||
path.Join(site.DataDir, "publish", page.ReadOnlyName + ".json"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return page, nil
|
||||
} else if err != nil {
|
||||
// Throw any other errors
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse JSON file
|
||||
err = json.Unmarshal(pageJson, &page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return page, err
|
||||
}
|
||||
|
||||
func (site *Site) LoadPublished(readOnlyName string) (*Page, error) {
|
||||
page := new(Page)
|
||||
filename := path.Join(site.DataDir, "publish", readOnlyName + ".json")
|
||||
|
||||
// Read file
|
||||
fileContents, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse JSON file
|
||||
err = json.Unmarshal(fileContents, &page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (p *Page) Update(newRawContent string) error {
|
||||
newRawContent = strings.TrimRight(newRawContent, "\n\t ")
|
||||
p.RawContent.Update(newRawContent)
|
||||
p.Render()
|
||||
return p.Save()
|
||||
}
|
||||
|
||||
func (p *Page) Render() {
|
||||
p.RenderedContent = MarkdownToHtml(p.RawContent.GetCurrent())
|
||||
}
|
||||
|
||||
func (p *Page) Save() error {
|
||||
p.Site.SaveMutex.Lock()
|
||||
defer p.Site.SaveMutex.Unlock()
|
||||
var tmp struct {
|
||||
Name string
|
||||
ReadOnlyName string
|
||||
RawContent versionedtext.VersionedText
|
||||
RenderedContent string
|
||||
}
|
||||
tmp.Name = p.Name
|
||||
tmp.RawContent = p.RawContent
|
||||
tmp.RenderedContent = p.RenderedContent
|
||||
tmp.ReadOnlyName = p.ReadOnlyName
|
||||
bJSON, err := json.MarshalIndent(tmp, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(p.FileName(), bJSON, 0644)
|
||||
}
|
||||
|
||||
func (p *Page) Erase() error {
|
||||
p.Site.Logger.Trace("Erasing " + p.Name)
|
||||
return os.Remove(p.FileName())
|
||||
}
|
||||
|
||||
func (p *Page) FileName() string {
|
||||
return path.Join(p.Site.DataDir, "pages", p.Name+".json")
|
||||
}
|
@ -0,0 +1,300 @@
|
||||
package notes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jcelliott/lumber"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Site struct {
|
||||
DataDir string
|
||||
AllowFileUploads bool
|
||||
MaxUploadSizeInMB uint
|
||||
Logger *lumber.ConsoleLogger
|
||||
MaxPageContentSizeInMB uint
|
||||
SaveMutex sync.Mutex
|
||||
}
|
||||
|
||||
func (site Site) Run(host string, port string) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.Default()
|
||||
router.SetFuncMap(template.FuncMap{
|
||||
"sniffContentType": site.sniffContentType,
|
||||
})
|
||||
router.LoadHTMLGlob("templates/*")
|
||||
router.GET("/", site.GetIndex)
|
||||
router.GET("/:page", site.GetPage)
|
||||
router.GET("/:page/*command", site.GetPageCommand)
|
||||
router.POST("/uploads", site.PostUploads)
|
||||
router.POST("/update", site.PostUpdate)
|
||||
log.Printf("Listening on %s:%s\n", host, port)
|
||||
panic(router.Run(host + ":" + port))
|
||||
}
|
||||
|
||||
func (site *Site) GetIndex(ctx *gin.Context) {
|
||||
ctx.Redirect(302, randomString(32)+"/edit")
|
||||
}
|
||||
|
||||
func (site *Site) GetPage(ctx *gin.Context) {
|
||||
page := ctx.Param("page")
|
||||
if page == "favicon.ico" {
|
||||
return
|
||||
}
|
||||
ctx.Redirect(302, page+"/view")
|
||||
}
|
||||
|
||||
func (site *Site) GetPageCommand(ctx *gin.Context) {
|
||||
page := ctx.Param("page")
|
||||
command := ctx.Param("command")
|
||||
if page == "uploads" {
|
||||
site.GetUploads(ctx, command)
|
||||
return
|
||||
}
|
||||
if page == "static" {
|
||||
ctx.File(path.Join("static", command))
|
||||
return
|
||||
}
|
||||
if page == "p" {
|
||||
site.GetPublished(ctx, command)
|
||||
return
|
||||
}
|
||||
switch command {
|
||||
case "/edit":
|
||||
site.GetPageEdit(ctx)
|
||||
case "/erase":
|
||||
site.GetPageErase(ctx)
|
||||
case "/view":
|
||||
site.GetPageView(ctx)
|
||||
case "/raw":
|
||||
site.GetPageRaw(ctx)
|
||||
case "/history":
|
||||
site.GetPageHistory(ctx)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (site *Site) GetPublished(ctx *gin.Context, readOnlyName string) {
|
||||
page, err := site.LoadPublished(readOnlyName)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
log.Println(page)
|
||||
ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"ReadPage": true,
|
||||
"RenderedContent": template.HTML(page.RenderedContent),
|
||||
})
|
||||
}
|
||||
|
||||
func (site *Site) GetPageView(ctx *gin.Context) {
|
||||
pageName := ctx.Param("page")
|
||||
page, err := site.Load(pageName)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
rawText := page.RawContent.GetCurrent()
|
||||
rawHTML := page.RenderedContent
|
||||
|
||||
// Check to see if an old version is requested
|
||||
version := ctx.DefaultQuery("version", "invalid")
|
||||
versionInt, versionErr := strconv.Atoi(version)
|
||||
if versionErr == nil && versionInt > 0 {
|
||||
versionText, err := page.RawContent.GetPreviousByTimestamp(int64(versionInt))
|
||||
if err == nil {
|
||||
rawText = versionText
|
||||
rawHTML = MarkdownToHtml(rawText)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"ViewPage": true,
|
||||
"Name": pageName,
|
||||
"ReadOnlyName": page.ReadOnlyName,
|
||||
"RenderedContent": template.HTML(rawHTML),
|
||||
})
|
||||
}
|
||||
|
||||
func (site *Site) GetPageEdit(ctx *gin.Context) {
|
||||
pageName := ctx.Param("page")
|
||||
page, err := site.Load(pageName)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"EditPage": true,
|
||||
"Name": pageName,
|
||||
"ReadOnlyName": page.ReadOnlyName,
|
||||
"RenderedContent": template.HTML(page.RenderedContent),
|
||||
"RawContent": page.RawContent.GetCurrent(),
|
||||
"CurrentUnixTime": time.Now().Unix(),
|
||||
"AllowFileUploads": site.AllowFileUploads,
|
||||
"MaxUploadSizeInMB": site.MaxUploadSizeInMB,
|
||||
})
|
||||
}
|
||||
|
||||
func (site *Site) GetPageRaw(ctx *gin.Context) {
|
||||
pageName := ctx.Param("page")
|
||||
page, err := site.Load(pageName)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
ctx.Writer.Header().Set("Content-Type", "text/plain")
|
||||
ctx.Data(200, contentType(page.Name), []byte(page.RawContent.GetCurrent()))
|
||||
}
|
||||
|
||||
func (site *Site) GetPageErase(ctx *gin.Context) {
|
||||
pageName := ctx.Param("page")
|
||||
page, err := site.Load(pageName)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
page.Erase()
|
||||
ctx.Redirect(302, pageName+"/edit")
|
||||
}
|
||||
|
||||
func (site *Site) GetPageHistory(ctx *gin.Context) {
|
||||
pageName := ctx.Param("page")
|
||||
page, err := site.Load(pageName)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
timestamps, changeSums := page.RawContent.GetMajorSnapshotsAndChangeSums(60)
|
||||
n := len(timestamps)
|
||||
reversedTimestamps := make([]int64, n)
|
||||
reversedChangeSums := make([]int, n)
|
||||
reversedFormattedNames := make([]string, n)
|
||||
for i, v := range timestamps {
|
||||
reversedTimestamps[n-i-1] = timestamps[i]
|
||||
reversedChangeSums[n-i-1] = changeSums[i]
|
||||
reversedFormattedNames[n-i-1] = time.Unix(v/1000000000, 0).Format("January 2, 2006 15:04:05 MST")
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"HistoryPage": true,
|
||||
"Name": pageName,
|
||||
"ReadOnlyName": page.ReadOnlyName,
|
||||
"VersionTimestamps": reversedTimestamps,
|
||||
"VersionFormattedNames": reversedFormattedNames,
|
||||
"VersionChangeSums": reversedChangeSums,
|
||||
})
|
||||
}
|
||||
|
||||
func (site *Site) PostUpdate(c *gin.Context) {
|
||||
var json struct {
|
||||
Page string `json:"page"`
|
||||
RawContent string `json:"new_text"`
|
||||
FetchedAt int64 `json:"fetched_at"`
|
||||
}
|
||||
err := c.BindJSON(&json)
|
||||
if err != nil {
|
||||
site.Logger.Trace(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "Error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check content length
|
||||
if uint(len(json.RawContent)) > site.MaxPageContentSizeInMB {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "Content too long",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p, err := site.Load(json.Page)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check concurrent editing
|
||||
if json.FetchedAt > 0 && p.LastEditUnixTime() > json.FetchedAt {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "Refusing to overwrite others work",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = p.Update(json.RawContent)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = p.Save()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Saved",
|
||||
"unix_time": time.Now().Unix(),
|
||||
"rendered": p.RenderedContent,
|
||||
})
|
||||
}
|
||||
|
||||
func (site *Site) PostUploads(c *gin.Context) {
|
||||
if !site.AllowFileUploads {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("uploads are disabled on this server"))
|
||||
return
|
||||
}
|
||||
|
||||
file, info, err := c.Request.FormFile("file")
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
newName := randomString(64)
|
||||
outfile, err := os.Create(path.Join(site.DataDir, "uploads", newName+".upload"))
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
file.Seek(0, io.SeekStart)
|
||||
_, err = io.Copy(outfile, file)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Location", "/uploads/"+newName+"?filename="+url.QueryEscape(info.Filename))
|
||||
return
|
||||
}
|
||||
|
||||
func (site *Site) GetUploads(ctx *gin.Context, filename string) {
|
||||
if !strings.HasSuffix(filename, ".upload") {
|
||||
filename = filename + ".upload"
|
||||
}
|
||||
pathname := path.Join(site.DataDir, "uploads", filename)
|
||||
ctx.Header("Content-Type", "text/plain")
|
||||
ctx.Header(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="`+ctx.DefaultQuery("filename", "upload")+`"`,
|
||||
)
|
||||
ctx.File(pathname)
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package notes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
mathjax "github.com/litao91/goldmark-mathjax"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var bmPolicy *bluemonday.Policy
|
||||
var md goldmark.Markdown
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
bmPolicy = bluemonday.UGCPolicy()
|
||||
bmPolicy.RequireNoReferrerOnLinks(true)
|
||||
bmPolicy.AllowElements("input")
|
||||
bmPolicy.AllowElements("style")
|
||||
bmPolicy.AllowAttrs("checked", "disabled", "type").OnElements("input")
|
||||
bmPolicy.AllowAttrs("width", "height", "align").OnElements("img")
|
||||
bmPolicy.AllowAttrs("style", "class", "align").OnElements("span", "p", "div", "a")
|
||||
md = goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM),
|
||||
goldmark.WithExtensions(mathjax.MathJax),
|
||||
goldmark.WithExtensions(extension.Typographer),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithHardWraps(),
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
s := make([]rune, n)
|
||||
for i := range s {
|
||||
s[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func contentType(filename string) string {
|
||||
switch {
|
||||
case strings.Contains(filename, ".css"):
|
||||
return "text/css"
|
||||
case strings.Contains(filename, ".jpg"):
|
||||
return "image/jpeg"
|
||||
case strings.Contains(filename, ".png"):
|
||||
return "image/png"
|
||||
case strings.Contains(filename, ".js"):
|
||||
return "application/javascript"
|
||||
case strings.Contains(filename, ".xml"):
|
||||
return "application/xml"
|
||||
}
|
||||
return "text/html"
|
||||
}
|
||||
|
||||
func (site *Site) sniffContentType(name string) (string, error) {
|
||||
file, err := os.Open(path.Join(site.DataDir, name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Only the first 512 bytes are used to sniff the content type.
|
||||
buffer := make([]byte, 512)
|
||||
_, err = file.Read(buffer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Always returns a valid content-type and "application/octet-stream" if no others seemed to match.
|
||||
return http.DetectContentType(buffer), nil
|
||||
}
|
||||
|
||||
func exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func MarkdownToHtml(s string) string {
|
||||
var output bytes.Buffer
|
||||
if err := md.Convert([]byte(s), &output); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bmPolicy.SanitizeReader(&output).String()
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
"use strict";
|
||||
|
||||
$(window).load(function () {
|
||||
let userInput = $('#userInput');
|
||||
let saveEditButton = $('#saveEditButton');
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
// leading edge, instead of the trailing.
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function () {
|
||||
saveEditButton.removeClass()
|
||||
saveEditButton.text("Editing");
|
||||
var context = this,
|
||||
args = arguments;
|
||||
var later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
}
|
||||
|
||||
// This will apply the debounce effect on the keyup event
|
||||
// And it only fires 500ms or half a second after the user stopped typing
|
||||
var prevText = userInput.val();
|
||||
console.log("debounce: " + window.notes.debounceMS)
|
||||
userInput.on('keyup', debounce(function () {
|
||||
if (prevText === userInput.val()) {
|
||||
return // no changes
|
||||
}
|
||||
prevText = userInput.val();
|
||||
saveEditButton.removeClass()
|
||||
saveEditButton.text("Saving")
|
||||
upload();
|
||||
}, window.notes.debounceMS));
|
||||
|
||||
var latestUpload = null, needAnother = false;
|
||||
|
||||
function upload() {
|
||||
// Prevent concurrent uploads
|
||||
if (latestUpload != null) {
|
||||
needAnother = true;
|
||||
return
|
||||
}
|
||||
latestUpload = $.ajax({
|
||||
type: 'POST',
|
||||
url: '/update',
|
||||
data: JSON.stringify({
|
||||
new_text: userInput.val(),
|
||||
page: window.notes.pageName,
|
||||
fetched_at: window.lastFetch,
|
||||
}),
|
||||
success: function (data) {
|
||||
latestUpload = null;
|
||||
saveEditButton.removeClass()
|
||||
if (data.success === true) {
|
||||
$("#rendered").html(data.rendered);
|
||||
retagContent();
|
||||
saveEditButton.addClass("success");
|
||||
window.lastFetch = data.unix_time;
|
||||
if (needAnother) {
|
||||
upload();
|
||||
}
|
||||
} else {
|
||||
saveEditButton.addClass("failure");
|
||||
}
|
||||
saveEditButton.text(data.message);
|
||||
needAnother = false;
|
||||
},
|
||||
error: function (xhr, error) {
|
||||
latestUpload = null;
|
||||
needAnother = false;
|
||||
saveEditButton.removeClass()
|
||||
saveEditButton.addClass("failure");
|
||||
saveEditButton.text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
retagContent();
|
||||
});
|
||||
|
||||
function retagContent() {
|
||||
// Render checkmarks
|
||||
$("li:has(input)").addClass("checkmark done");
|
||||
$("ul:has(input)").addClass("checkmark");
|
||||
$("li.checkmark:has(input:not(:checked))").removeClass("done");
|
||||
$("input").click(function () {
|
||||
return false;
|
||||
});
|
||||
|
||||
// Fix page title
|
||||
let h1 = $("h1");
|
||||
if (h1.length > 0) {
|
||||
document.title = h1.first().text();
|
||||
}
|
||||
|
||||
// Center tables
|
||||
$("table").wrap("<div class='table-wrapper'></div>");
|
||||
|
||||
// Re-render LaTeX equations
|
||||
window.MathJax.texReset();
|
||||
window.MathJax.typesetClear();
|
||||
window.MathJax.typeset();
|
||||
|
||||
// Re-render Mermaid diagrams
|
||||
mermaid.init()
|
||||
|
||||
}
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
function onUploadFinished(file) {
|
||||
let userInput = $('#userInput');
|
||||
this.removeFile(file);
|
||||
var cursorPos = userInput.prop('selectionStart');
|
||||
var cursorEnd = userInput.prop('selectionEnd');
|
||||
var v = userInput.val();
|
||||
var textBefore = v.substring(0, cursorPos);
|
||||
var textAfter = v.substring(cursorPos, v.length);
|
||||
var message = 'uploaded file';
|
||||
if (cursorEnd > cursorPos) {
|
||||
message = v.substring(cursorPos, cursorEnd);
|
||||
textAfter = v.substring(cursorEnd, v.length);
|
||||
}
|
||||
var prefix = '';
|
||||
if (file.type.startsWith("image")) {
|
||||
prefix = '!';
|
||||
}
|
||||
var extraText = prefix + '[' + file.xhr.getResponseHeader("Location").split('filename=')[1] + '](' +
|
||||
file.xhr.getResponseHeader("Location") +
|
||||
')';
|
||||
|
||||
userInput.val(
|
||||
textBefore +
|
||||
extraText +
|
||||
textAfter
|
||||
);
|
||||
|
||||
// Select the newly-inserted link
|
||||
userInput.prop('selectionStart', cursorPos);
|
||||
userInput.prop('selectionEnd', cursorPos + extraText.length);
|
||||
userInput.trigger('keyup'); // trigger a save
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
from time import sleep
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
INDEX_URL = "http://localhost:8050"
|
||||
|
||||
|
||||
def _launch():
|
||||
options = Options()
|
||||
options.add_argument("--headless")
|
||||
options.add_argument("window-size=1920,1080")
|
||||
browser = webdriver.Chrome(options=options)
|
||||
return browser
|
||||
|
||||
|
||||
def test_index_should_redirect_to_edit():
|
||||
browser = _launch()
|
||||
browser.get(INDEX_URL)
|
||||
assert "/edit" in browser.current_url
|
||||
browser.close()
|
||||
|
||||
|
||||
def test_should_edit():
|
||||
browser = _launch()
|
||||
browser.get(INDEX_URL)
|
||||
|
||||
# Type a new note
|
||||
user_input = browser.find_element_by_id("userInput")
|
||||
user_input.clear()
|
||||
user_input.send_keys(
|
||||
"# Hello world\n\n"
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec feugiat "
|
||||
"euismod nibh, ac scelerisque erat laoreet quis. Nullam dignissim varius "
|
||||
"enim. Aenean interdum et elit eu gravida. Cras eleifend eget tortor sit amet "
|
||||
"tincidunt. Praesent eu interdum turpis. Nullam et massa massa. Maecenas "
|
||||
"maximus turpis id egestas rhoncus. Morbi eget bibendum leo. "
|
||||
)
|
||||
sleep(1)
|
||||
|
||||
# Should render the preview
|
||||
h1 = browser.find_element_by_css_selector("h1")
|
||||
assert h1.text == "Hello world"
|
||||
|
||||
# Refresh should not delete the content
|
||||
browser.refresh()
|
||||
assert "Lorem ipsum" in browser.page_source
|
||||
|
||||
# Click view and verify content
|
||||
browser.find_element_by_link_text("View").click()
|
||||
assert "/view" in browser.current_url
|
||||
h1 = browser.find_element_by_css_selector("h1")
|
||||
assert h1.text == "Hello world"
|
||||
assert "Lorem ipsum" in browser.page_source
|
||||
assert "Hello world" in browser.title
|
||||
|
||||
# Click publish and verify content
|
||||
browser.find_element_by_link_text("Publish").click()
|
||||
h1 = browser.find_element_by_css_selector("h1")
|
||||
assert h1.text == "Hello world"
|
||||
assert "Hello world" in browser.title
|
||||
|
||||
# End session
|
||||
browser.close()
|
@ -0,0 +1,2 @@
|
||||
selenium==3.141.0
|
||||
pytest==5.4.3
|
@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
|
||||
<link rel="stylesheet" href="/static/lib/dropzone.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/lib/github-markdown.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/lib/menus-min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/lib/base-min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/lib/highlight.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/notes.css">
|
||||
<script type="text/javascript" src="/static/lib/jquery.min.js"></script>
|
||||
<script src="/static/lib/highlight.min.js"></script>
|
||||
<script type="text/javascript" src="/static/lib/highlight.pack.js"></script>
|
||||
<script src="/static/lib/dropzone.js"></script>
|
||||
<script id="MathJax-script" async src="/static/lib/mathjax/es5/tex-mml-chtml.js"></script>
|
||||
<script src="/static/lib/mermaid.min.js"></script>
|
||||
|
||||
<title>{{ .Name }}</title>
|
||||
|
||||
<script type='text/javascript'>
|
||||
hljs.initHighlightingOnLoad();
|
||||
window.notes = {
|
||||
debounceMS: 500,
|
||||
lastFetch: {{ .CurrentUnixTime }},
|
||||
pageName: "{{ .Name }}",
|
||||
}
|
||||
window.MathJax = {
|
||||
tex: {
|
||||
tags: 'ams'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script type="text/javascript" src="/static/notes.bundle.js"></script>
|
||||
</head>
|
||||
<body class="
|
||||
{{ if .EditPage }} EditPage {{ end }}
|
||||
{{ if .ViewPage }} ViewPage {{ end }}
|
||||
{{ if .HistoryPage }} HistoryPage {{ end }}
|
||||
{{ if .ReadPage }} ReadPage {{ end }}
|
||||
">
|
||||
|
||||
{{ if .ReadPage }}
|
||||
<!-- No menu for read page -->
|
||||
{{ else }}
|
||||
<div id="menu">
|
||||
<ul>
|
||||
<li><a href="/">New</a></li>
|
||||
{{ if .ViewPage }}
|
||||
<li class="selected">View</li>
|
||||
{{ else }}
|
||||
<li><a href="/{{ .Name }}/view">View</a></li>
|
||||
{{ end }}
|
||||
{{ if .EditPage }}
|
||||
<li class="selected"><span id="saveEditButton">Edit</span></li>
|
||||
{{ else }}
|
||||
<li><a href="/{{ .Name }}/edit">Edit</a></li>
|
||||
{{ end }}
|
||||
{{ if .HistoryPage }}
|
||||
<li class="selected">History</li>
|
||||
{{ else }}
|
||||
<li><a href="/{{ .Name }}/history">History</a></li>
|
||||
{{ end }}
|
||||
<li><a href="/p/{{ .ReadOnlyName }}">Publish</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
<article class="markdown-body">
|
||||
<div id="wrap">
|
||||
{{ if .EditPage }}
|
||||
<div id="pad">
|
||||
<script>
|
||||
Dropzone.options.userInputForm = {
|
||||
clickable: false,
|
||||
maxFilesize: {{ if .MaxUploadSizeInMB }} {{.MaxUploadSizeInMB}} {{ else }} 10 {{end }}, // MB
|
||||
init: function initDropzone() {
|
||||
this.on("complete", onUploadFinished);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<form
|
||||
id="userInputForm"
|
||||
action="/uploads"
|
||||
{{ if .AllowFileUploads }}
|
||||
class="dropzone"
|
||||
{{ end }}
|
||||
>
|
||||
<textarea
|
||||
autofocus
|
||||
placeholder="Use markdown to write your note!"
|
||||
id="userInput"
|
||||
>{{ .RawContent }}</textarea>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div id="rendered">
|
||||
{{ .RenderedContent }}
|
||||
{{ if .HistoryPage }}
|
||||
<h1>History</h1>
|
||||
<ul class="history">
|
||||
{{range $i, $e := .VersionTimestamps}}
|
||||
<li>
|
||||
<a href="/{{ $.Name }}/view?version={{index $.VersionTimestamps $i}}">{{index $.VersionFormattedNames $i}}</a>
|
||||
(<span>{{index $.VersionChangeSums $i}}</span>)
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{ end }}
|
||||
</div>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in new issue