Initial version

pull/1/head
Alinson S. Xavier 5 years ago
commit 5d302bd792

@ -0,0 +1,5 @@
data*
build*
node_modules
Dockerfile
src/python

1
.gitattributes vendored

@ -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

25
.gitignore vendored

@ -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.
```

11
lib/base-min.css vendored

@ -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}

388
lib/dropzone.css vendored

@ -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; }

3504
lib/dropzone.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1
lib/highlight.css vendored

@ -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

7
lib/menus-min.css vendored

@ -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}

1168
package-lock.json generated

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…
Cancel
Save