Compare commits

..

67 Commits

Author SHA1 Message Date
da11811e2d Problem: indexing search objects is slow
All checks were successful
continuous-integration/drone/push Build is passing
Solution: batch indexing search objects
2022-05-26 21:18:19 +02:00
19183da0f8 Problem: block repo did not serialize writes
Solution: serialize writes
2022-05-26 21:17:51 +02:00
1acd0e5f0f Problem: gitRevision does not handle errors
Solution: handle errors in gitRevision
2022-05-01 22:11:58 +02:00
3ee280a124 Problem: can't fetch a block with the API
Solution: add route to fetch a single with the API
2022-05-01 22:01:53 +02:00
726f3c944b Problem: negative branch was first
Solution: positive branch is first
2022-05-01 22:01:27 +02:00
e7d2c0e1a9 Problem: Markdown takes some time to process
All checks were successful
continuous-integration/drone/push Build is passing
Solution: Don't process when it's not Markdown
2022-04-29 21:42:06 +02:00
66fe665bf2 Problem: graph is slow
Solution: disable graph
2022-04-29 21:41:49 +02:00
79aba57dcb Problem: moving up from closed item, moved too far
All checks were successful
continuous-integration/drone/push Build is passing
Solution: add tests for this problem, and remove indent code when moving
up
2022-04-29 21:27:57 +02:00
f617dd973f Problem: need url for login
All checks were successful
continuous-integration/drone/push Build is passing
Solution: create url when input does not parse
2022-04-27 20:44:21 +02:00
b33cfeb395 Problem: we don't have SR
Some checks reported errors
continuous-integration/drone/push Build was killed
Solution: add SR
2022-04-27 20:43:45 +02:00
569bef3226 Problem: tags are not parsed in the backend
All checks were successful
continuous-integration/drone/push Build is passing
Solution: replace tag with markdown links
2022-01-17 21:43:17 +01:00
8fa7d4170f Problem: sr.js is messy
Solution: cleanup sr.js and extract functions
2022-01-17 21:34:35 +01:00
d584fe8bf7 Problem: tags are not really visible
All checks were successful
continuous-integration/drone/push Build is passing
Solution: show tags as tags
2022-01-17 21:06:00 +01:00
f79a01ae9b Problem: tags are not parsed server side
Solution: add parsing of tags to server side
2022-01-17 21:01:34 +01:00
71d957ae9b Problem: mark markup uses "=="
Solution: remove "==" around mark
2022-01-17 21:01:02 +01:00
27579e841e Problem: tags are not parsed on client side
Solution: parse tags in markdown
2022-01-17 20:56:37 +01:00
3d249dde05 Problem: block.go is missing and blockRepo could not be found
All checks were successful
continuous-integration/drone/push Build is passing
Solution: add block.go
2022-01-17 00:56:16 +01:00
40dfa46d7c Problem: sr.js is missing
Some checks failed
continuous-integration/drone/push Build is failing
Solution: add sr.js
2022-01-17 00:53:38 +01:00
b4a721ffeb Problem: the search query does not use _ for spaces when creating a new page
Some checks failed
continuous-integration/drone/push Build is failing
Solution: create a new page with _ for spaces
2022-01-17 00:50:31 +01:00
1b8b5ff4f6 Problem: checkbox part of file is not indented
Some checks failed
continuous-integration/drone/push Build is failing
Solution: fix indentation
2022-01-17 00:16:41 +01:00
f6566f6313 Problem: burger menu does not work on mobile
Solution: add even handler to open/close hamburger menu
2022-01-17 00:15:52 +01:00
6c7b66f4ab Problem: wiki does not support spaced repetition
Solution: implement spaced repetition for review of blocks
2022-01-17 00:15:00 +01:00
7d898afb03 Problem: array meta and single char meta keywords are not rendered right
All checks were successful
continuous-integration/drone/push Build is passing
Solution: fix problem with array meta and single char meta keywords
2022-01-15 17:00:46 +01:00
564081a581 Problem: lists generate with *
All checks were successful
continuous-integration/drone/push Build is passing
Solution: generate lists for -
2022-01-15 16:52:55 +01:00
8af3d22d06 Problem: keywords could not contain a space
Solution: allow space in keywords
2022-01-15 16:52:26 +01:00
965d2c87f5 Problem: tags are not very visible
All checks were successful
continuous-integration/drone/push Build is passing
Solution: improve visibility of tags with a bit of color
2022-01-15 16:35:57 +01:00
d22f3e123a Problem: TODO and DONE are shown a normal text links
Solution: Replace TODO and DONE tags with checkboxes
2022-01-15 16:31:47 +01:00
a8fb7f62ad Problem: Keywords are links in read-only view
All checks were successful
continuous-integration/drone/push Build is passing
Solution: Don't like the keywords in read-only view
2022-01-15 16:22:17 +01:00
d569fd560f Problem: search results have edit link while on read side
All checks were successful
continuous-integration/drone/push Build is passing
Solution: only use /edit/ link when editing
2022-01-15 02:04:49 +01:00
1115fa14ff Problem: mobile view is read/write
All checks were successful
continuous-integration/drone/push Build is passing
Solution: make a read only version for mobile
2022-01-15 01:46:10 +01:00
39a3a9d270 Problem: links are not clickable
All checks were successful
continuous-integration/drone/push Build is passing
Solution: auto-link links in markdown
2022-01-15 01:24:30 +01:00
cff499800e Copy and paste HTML as Markdown 2022-01-15 01:24:16 +01:00
f3fa3a0a91 Problem: first line of pasted text is not saved
All checks were successful
continuous-integration/drone/push Build is passing
Solution: change regex of input string split
2022-01-12 23:58:36 +01:00
d3cde5fd94 Problem: lines for - are not supported in copy paste
All checks were successful
continuous-integration/drone/push Build is passing
Solution: adjust regex so as not to split there
2022-01-12 22:51:54 +01:00
e4539b520c Problem: some standard copy paste formats do not work
All checks were successful
continuous-integration/drone/push Build is passing
Solution: add solution for copying text from Roam. This formats looks
like:
    - parent
        - child 1
        - child 2

The first line starts with a dash and space. The other lines also start
with spaces (multiple of 4 or 0), dash, space. Every 4 spaces is 1
indent in our format.
2022-01-12 22:45:46 +01:00
2ed65e417f Problem: there is not much space at the bottom of the page
Solution: add a large margin-top on the footer
2022-01-12 22:45:13 +01:00
293c9d66a4 Problem: fold arrow is small
Solution: increase size of clickable area for arrow
2022-01-12 21:32:53 +01:00
c30156dd10 Problem: copy and paste always creates new nodes
All checks were successful
continuous-integration/drone/push Build is passing
Solution: allow one line text as normals text
2022-01-09 23:46:54 +01:00
e743576043 Problem: block markdown is not supported
Solution: render cells as block markdown
2022-01-09 23:46:24 +01:00
e2aa173432 Problem: can't copy and paste multiline text as multiple items
All checks were successful
continuous-integration/drone/push Build is passing
Solution: split text and add multiple items
2022-01-09 22:15:48 +01:00
258dd4f7ab Problem: search does not work with dates and tags
All checks were successful
continuous-integration/drone/push Build is passing
Solution: add search for tags and dates
2022-01-08 21:58:41 +01:00
fa9f34e42f Problem: you can not search for lines without grouping
All checks were successful
continuous-integration/drone/push Build is passing
Solution: add search operator "query@" to find lines without grouping on
titles.
2022-01-07 21:39:01 +01:00
cba1d002d9 Problem: there are no suggest date pages in links
All checks were successful
continuous-integration/drone/push Build is passing
Solution: add Suggested page with date page to page references
2022-01-07 21:17:04 +01:00
878936ec13 Problem: bleve was old version
All checks were successful
continuous-integration/drone/push Build is passing
Solution: upgrade blevesearch to 2.3.0
2022-01-07 21:08:23 +01:00
d7d205f502 Problem: we don't support natural language dates in page search
All checks were successful
continuous-integration/drone/push Build is passing
Solution: add natural language date parser and allow to create new pages
2022-01-07 20:58:43 +01:00
3c53e14229 Problem: there are no easy ways to create a page from search
Solution: add create new page option to search results
2022-01-07 20:37:44 +01:00
1f6a22966f Problem: metadata links are not parsed and rendered
All checks were successful
continuous-integration/drone/push Build is passing
Solution: create parser and renderer for metadata links.
2021-11-14 23:36:34 +01:00
74b1220710 Use a random seed to always generate new ids
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-06 14:37:19 +01:00
001e4748dd Use multiple underscore when there are multiple spaces 2021-11-06 14:37:03 +01:00
760eb11694 Implement "Create page from item" 2021-11-06 14:36:10 +01:00
67af38f785 Implement "silent" for markdown
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-29 22:59:33 +02:00
ccc3ac7c66 Apply relativeBaseURL to parse urls 2021-10-29 22:43:34 +02:00
a4bff99cee Fix TODO/DONE line-through
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-29 22:32:25 +02:00
b63eb7948f Add #tag "#" on links 2021-10-29 22:14:56 +02:00
70f1b62646 Improve links and checkboxes
All checks were successful
continuous-integration/drone/push Build is passing
Use the markdown parser to handle links and checkboxes instead of
internal javascript.

Also add catch clauses to the promises in the editor.
2021-10-29 21:40:49 +02:00
a2c83d87b6 Hide backrefs on editor page (are visible in sidebar)
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-25 21:03:38 +02:00
f8a94455cd Save folds closed/open status in localStorage
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-25 20:59:50 +02:00
e98a099127 Send outputData to beforeSave callback 2021-09-25 20:58:08 +02:00
8da06bc939 Adds tabs for calendar, graph and backlinks in right sidebar
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-04 22:33:32 +02:00
5a7067dc7d fix(store): move before and fix indent of multiple items
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-24 22:27:16 +02:00
c1fe438925 fix(drag): while dragging copy marginLeft to show item with right indent
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-24 22:02:56 +02:00
8167f6d85d fix(store): add test for moving to the last position
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-24 21:46:57 +02:00
22edb6165b fix(store): copy indent from next item after moving 2021-08-24 21:44:10 +02:00
38f3c58da9 Fix test for list-editor
All checks were successful
continuous-integration/drone/push Build is passing
Skip query check when no text is set
2021-08-24 21:30:41 +02:00
685fc26839 Add npm testing to pre-commit config
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-24 21:23:08 +02:00
96beb5c309 Run list-editor and editor tests
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-24 21:08:59 +02:00
3bdc30465e Improve link-complete dropdown menu
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-22 22:50:33 +02:00
40 changed files with 1937 additions and 542 deletions

View File

@ -37,8 +37,10 @@ steps:
commands: commands:
- cd list-editor - cd list-editor
- npm install - npm install
- npm test
- cd ../editor - cd ../editor
- npm install - npm install
- npm test
- npm run docker - npm run docker
- name: rebuild-cache-with-filesystem - name: rebuild-cache-with-filesystem

View File

@ -19,3 +19,11 @@ repos:
language: system language: system
entry: drone lint --trusted entry: drone lint --trusted
files: .drone.yml files: .drone.yml
- id: npm-test-list-editor
name: npm test list editor
language: system
entry: bash -c "cd list-editor && npm test"
- id: npm-test-editor
name: npm test editor
language: system
entry: bash -c "cd editor && npm test"

92
block.go Normal file
View File

@ -0,0 +1,92 @@
/*
* Wiki - A wiki with editor
* Copyright (c) 2021-2021 Peter Stuifzand
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
type BlockRepository interface {
Save(id string, block Block) error
Load(id string) (Block, error)
}
type blockSaveMessage struct {
id string
block Block
}
type blockRepo struct {
dirname string
saveC chan blockSaveMessage
errC chan error
}
func saveBlock(dirname, id string, block Block) error {
f, err := os.OpenFile(filepath.Join(dirname, BlocksDirectory, id), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(&block)
if err != nil {
return err
}
return nil
}
func NewBlockRepo(dirname string) BlockRepository {
saveC := make(chan blockSaveMessage, 1)
errC := make(chan error)
go func() {
for msg := range saveC {
err := saveBlock(dirname, msg.id, msg.block)
errC <- err
}
}()
return &blockRepo{
dirname: dirname,
saveC: saveC,
errC: errC,
}
}
func (br *blockRepo) Save(id string, block Block) error {
br.saveC <- blockSaveMessage{id, block}
err := <-br.errC
return err
}
func (br *blockRepo) Load(id string) (Block, error) {
f, err := os.Open(filepath.Join(br.dirname, BlocksDirectory, id))
if err != nil {
return Block{}, fmt.Errorf("%q: %w", id, BlockNotFound)
}
defer f.Close()
var block Block
err = json.NewDecoder(f).Decode(&block)
if err != nil {
return Block{}, fmt.Errorf("%q: %w", id, err)
}
return block, nil
}

158
blocks.go
View File

@ -1,158 +0,0 @@
/*
* Wiki - A wiki with editor
* Copyright (c) 2021 Peter Stuifzand
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sync"
)
const BlocksDirectory = "_blocks"
type FileBlockRepository struct {
dirname string
rwLock sync.RWMutex
}
type Block struct {
Text string
Children []string
Parent string
}
func NewBlockRepo(dirname string) (*FileBlockRepository, error) {
err := os.MkdirAll(filepath.Join(dirname, BlocksDirectory), 0777)
if err != nil {
return nil, err
}
return &FileBlockRepository{
dirname: dirname,
}, nil
}
func (blockRepo *FileBlockRepository) GetBlock(blockID string) (Block, error) {
blockRepo.rwLock.RLock()
defer blockRepo.rwLock.RUnlock()
return loadBlock(blockRepo.dirname, blockID)
}
func (blockRepo *FileBlockRepository) SaveBlock(blockID string, block Block) error {
blockRepo.rwLock.Lock()
defer blockRepo.rwLock.Unlock()
return saveBlock(blockRepo.dirname, blockID, block)
}
func (blockRepo *FileBlockRepository) GetBlocks(rootBlockID string) (BlockResponse, error) {
resp := BlockResponse{
rootBlockID,
"",
nil,
nil,
nil,
}
resp.Texts = make(map[string]string)
resp.Children = make(map[string][]string)
queue := []string{rootBlockID}
block, err := blockRepo.GetBlock(rootBlockID)
if err != nil {
return BlockResponse{}, err
}
// NOTE: what does this do?
if rootBlockID[0] != '_' && block.Children == nil {
return BlockResponse{}, fmt.Errorf("not a block and has no children: %w", BlockNotFound)
}
prevID := rootBlockID
parentID := block.Parent
for parentID != "root" {
parent, err := blockRepo.GetBlock(parentID)
if err != nil {
return BlockResponse{}, fmt.Errorf("while loading current parent block (%s->%s): %w", prevID, parentID, err)
}
resp.Texts[parentID] = parent.Text
resp.Children[parentID] = parent.Children
resp.ParentID = parentID
resp.Parents = append(resp.Parents, parentID)
prevID = parentID
parentID = parent.Parent
}
if parentID == "root" {
resp.ParentID = "root"
}
for {
if len(queue) == 0 {
break
}
current := queue[0]
queue = queue[1:]
block, err := blockRepo.GetBlock(current)
if err != nil {
return BlockResponse{}, err
}
resp.Texts[current] = block.Text
resp.Children[current] = block.Children
queue = append(queue, block.Children...)
}
return resp, nil
}
func loadBlock(dirname, blockID string) (Block, error) {
f, err := os.Open(filepath.Join(dirname, BlocksDirectory, blockID))
if err != nil {
return Block{}, fmt.Errorf("%q: %w", blockID, BlockNotFound)
}
defer f.Close()
var block Block
err = json.NewDecoder(f).Decode(&block)
if err != nil {
return Block{}, fmt.Errorf("%q: %v", blockID, err)
}
return block, nil
}
func saveBlock(dirname, blockID string, block Block) error {
log.Printf("Writing to %q\n", blockID)
f, err := os.OpenFile(filepath.Join(dirname, BlocksDirectory, blockID), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("while saving block %s: %w", blockID, err)
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(&block)
if err != nil {
return fmt.Errorf("while encoding block %s: %w", blockID, err)
}
return nil
}

View File

@ -1895,6 +1895,14 @@
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
"dev": true "dev": true
}, },
"chrono-node": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.3.5.tgz",
"integrity": "sha512-QIWEgXYVn55/Nsgdqbe6inqW+GoK3B6Qtga8AWdpq+nd+mOZVMxa+SGwPq/XjY+nKN+toQGu8KifCPwUkmz2sg==",
"requires": {
"dayjs": "^1.10.0"
}
},
"class-utils": { "class-utils": {
"version": "0.3.6", "version": "0.3.6",
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@ -2655,6 +2663,11 @@
"assert-plus": "^1.0.0" "assert-plus": "^1.0.0"
} }
}, },
"dayjs": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
"integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
},
"debug": { "debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",

View File

@ -8,6 +8,7 @@
"@egjs/hammerjs": "^2.0.17", "@egjs/hammerjs": "^2.0.17",
"axios": "^0.19.2", "axios": "^0.19.2",
"bulma": "^0.7.5", "bulma": "^0.7.5",
"chrono-node": "^2.3.5",
"clipboard": "^2.0.8", "clipboard": "^2.0.8",
"copy-text-to-clipboard": "^2.2.0", "copy-text-to-clipboard": "^2.2.0",
"css-loader": "^3.6.0", "css-loader": "^3.6.0",
@ -59,7 +60,7 @@
}, },
"scripts": { "scripts": {
"test": "node_modules/.bin/mocha -r esm", "test": "node_modules/.bin/mocha -r esm",
"watch": "webpack --watch", "watch": "webpack --watch --mode=development",
"start": "webpack-dev-server --open --hot", "start": "webpack-dev-server --open --hot",
"build": "webpack --progress --mode=production", "build": "webpack --progress --mode=production",
"docker": "webpack --no-color --mode=production" "docker": "webpack --no-color --mode=production"

View File

@ -36,7 +36,7 @@ function addSaver(editor, saveUrl, page, beforeSave) {
save() { save() {
return editor.save() return editor.save()
.then(outputData => { .then(outputData => {
beforeSave() beforeSave(outputData)
let data = { let data = {
'json': 1, 'json': 1,
'p': page, 'p': page,
@ -108,18 +108,12 @@ function showSearchResultsExtended(element, template, searchTool, query, input,
return searchTool(query).then(results => { return searchTool(query).then(results => {
let opt = options || {}; let opt = options || {};
if (opt.showOnlyResults && (query.length === 0 || !results.length)) { if (opt.showOnlyResults && (query.length === 0 || !results.length)) {
$lc.fadeOut() $lc.hide()
return return
} }
$lc.data('result-type', resultType) $lc.data('result-type', resultType)
if (opt.belowCursor) { const visible = $(':visible', $lc).length
let pos = getCaretCoordinates(input, value.selectionEnd, {})
let off = $(input).offset()
pos.top += off.top + pos.height
pos.left += off.left
$lc.offset(pos)
}
let selectedPos = 0; let selectedPos = 0;
let selected = $lc.find('li.selected'); let selected = $lc.find('li.selected');
@ -127,6 +121,8 @@ function showSearchResultsExtended(element, template, searchTool, query, input,
selectedPos = $lc.find('li').index(selected[0]) selectedPos = $lc.find('li').index(selected[0])
} }
const isEditing = window.location.pathname.match(/^\/edit\//)
let $ul = el('ul', let $ul = el('ul',
_.map(results, (hit, i) => { _.map(results, (hit, i) => {
let div = el('div', []); let div = el('div', []);
@ -139,7 +135,7 @@ function showSearchResultsExtended(element, template, searchTool, query, input,
]; ];
if (hit.ref) { if (hit.ref) {
children = hit.ref ? [el('a', children)] : children; children = hit.ref ? [el('a', children)] : children;
children[0].setAttribute('href', '/edit/' + hit.ref) children[0].setAttribute('href', (isEditing ? '/edit/' : '/') + hit.ref)
} }
const li = el('li', children) const li = el('li', children)
if (selectedPos === i) li.classList.add('selected') if (selectedPos === i) li.classList.add('selected')
@ -148,10 +144,17 @@ function showSearchResultsExtended(element, template, searchTool, query, input,
}) })
) )
$lc.html($ul).fadeIn() $lc.show()
.html($ul)
if (opt.belowCursor) {
let pos = getCaretCoordinates(input, value.selectionEnd, {})
let off = $(input).offset()
$lc.offset({top: off.top + pos.top + pos.height, left: off.left + pos.left})
}
return results return results
}) }).catch(e => console.log('searchtool', e))
} }
function formatLineResult(hits, key) { function formatLineResult(hits, key) {
@ -175,6 +178,17 @@ function formatLineResult(hits, key) {
] ]
} }
function formatLineWithoutTitleResult(hit, key) {
if (hit.line.match(/query[!@]?:/)) return []
return [{
text: hit.line,
indented: 0,
fold: 'open',
hidden: false,
fleeting: true
}]
}
function formatTitleResult(hit) { function formatTitleResult(hit) {
return [ return [
{ {
@ -283,7 +297,7 @@ function Editor(holder, input) {
return td return td
}) })
]) ])
}) }).catch(e => console.log('while fetching metakv', e))
}) })
}) })
.then(mflatten) .then(mflatten)
@ -312,8 +326,9 @@ function Editor(holder, input) {
div.classList.add('table-wrapper') div.classList.add('table-wrapper')
return element.html(div); return element.html(div);
}) }).catch(e => console.log('while creating table', e))
}) })
.catch(e => console.log('transformTable', e))
} }
async function transformMathExpression(converted, scope) { async function transformMathExpression(converted, scope) {
@ -349,13 +364,12 @@ function Editor(holder, input) {
} }
let converted = text let converted = text
let todo;
if (converted === '{{table}}') { if (converted === '{{table}}') {
transformTable.call(this, editor, id, element); transformTable.call(this, editor, id, element);
return return
} else if (converted.startsWith("```", 0) || converted.startsWith("$$", 0)) { } else if (converted.startsWith("```", 0) || converted.startsWith("$$", 0)) {
converted = MD.render(converted) converted = MD.renderInline(converted)
} else if (converted.startsWith("=", 0)) { } else if (converted.startsWith("=", 0)) {
transformMathExpression(converted, scope) transformMathExpression(converted, scope)
.then(converted => { .then(converted => {
@ -367,30 +381,35 @@ function Editor(holder, input) {
}) })
.catch(e => console.warn(e)) .catch(e => console.warn(e))
} else { } else {
let re = /^([A-Z0-9 ]+)::\s*(.*)$/i; // let re = /^([A-Z0-9 ]+)::\s*(.*)$/i;
let res = text.match(re) // let res = text.match(re)
if (res) { // if (res) {
converted = '<span class="metadata-key">[[' + res[1] + ']]</span>' // converted = '<span class="metadata-key">[[' + res[1] + ']]</span>'
if (res[2]) { // if (res[2]) {
converted += ': ' + res[2] // converted += ': ' + res[2]
} // }
} else if (text.match(/#\[\[TODO]]/)) { // } else if (text.match(/#\[\[TODO]]/)) {
converted = converted.replace('#[[TODO]]', '<input class="checkbox" type="checkbox" />') // converted = converted.replace('#[[TODO]]', '<input class="checkbox" type="checkbox" />')
todo = true; // todo = true;
} else if (text.match(/#\[\[DONE]]/)) { // } else if (text.match(/#\[\[DONE]]/)) {
converted = converted.replace('#[[DONE]]', '<input class="checkbox" type="checkbox" checked />') // converted = converted.replace('#[[DONE]]', '<input class="checkbox" type="checkbox" checked />')
todo = false; // todo = false;
} // }
MD.options.html = true converted = MD.render(converted)
converted = MD.renderInline(converted)
MD.options.html = false
} }
if (todo !== undefined) { try {
element.toggleClass('todo--done', todo === false) const todoItem = $.parseHTML(converted)
element.toggleClass('todo--todo', todo === true) if (todoItem.length && $(todoItem[0]).is(':checkbox')) {
} else { const todo = !$(todoItem[0]).is(':checked')
element.removeClass(['todo--todo', 'todo--done']) element.toggleClass('todo--done', todo === false)
element.toggleClass('todo--todo', todo === true)
} else {
element.removeClass(['todo--todo', 'todo--done'])
}
} catch (e) {
// problem with $(converted) is that it could be treated as a jQuery selector expression instead of normal text
// the wiki text is not quite like selectors
} }
element.html(converted) element.html(converted)
@ -428,10 +447,16 @@ function Editor(holder, input) {
let saveUrl = element.dataset.saveurl; let saveUrl = element.dataset.saveurl;
let page = element.dataset.page; let page = element.dataset.page;
let beforeSave = (curDoc) => {
indicator.setText('saving...')
}
addIndicator( addIndicator(
addSaver(editor, saveUrl, page, () => indicator.setText('saving...')), addSaver(editor, saveUrl, page, beforeSave),
indicator indicator
).save().then(() => indicator.done()) ).save()
.then(() => indicator.done())
.catch(e => console.log('editor.change', e))
}) })
// editor.on('rendered', function () { // editor.on('rendered', function () {
@ -514,13 +539,13 @@ function Editor(holder, input) {
const isVisible = $('#link-complete:visible').length > 0; const isVisible = $('#link-complete:visible').length > 0;
if (event.key === 'Escape' && isVisible) { if (event.key === 'Escape' && isVisible) {
$lc.fadeOut() $lc.hide()
return false return false
} else if (event.key === 'Enter' && isVisible) { } else if (event.key === 'Enter' && isVisible) {
const element = $lc.find('li.selected') const element = $lc.find('li.selected')
const linkName = element.text() const linkName = element.text()
$lc.trigger('popup:selected', [linkName, $lc.data('result-type'), element]) $lc.trigger('popup:selected', [linkName, $lc.data('result-type'), element])
$lc.fadeOut() $lc.hide()
return false return false
} else if (event.key === 'ArrowUp' && isVisible) { } else if (event.key === 'ArrowUp' && isVisible) {
const selected = $lc.find('li.selected') const selected = $lc.find('li.selected')
@ -580,7 +605,7 @@ function Editor(holder, input) {
if (event.key === '/') { if (event.key === '/') {
searchEnabled = true searchEnabled = true
} }
if (searchEnabled && event.key === 'Escape') { if (searchEnabled && (event.key === 'Escape' || event.key === 'Space')) {
searchEnabled = false searchEnabled = false
return false; return false;
} }
@ -607,18 +632,19 @@ function Editor(holder, input) {
if (insideSearch) { if (insideSearch) {
let query = value.substring(start + 1, end); let query = value.substring(start + 1, end);
showSearchResults(commandSearch, query, input, value, 'command').then(results => { showSearchResults(commandSearch, query, input, value, 'command')
if (query.length > 0 && results.length === 0) { .then(results => {
searchEnabled = false if (query.length > 0 && (!results || results.length === 0)) {
} searchEnabled = false
}) }
}).catch(e => console.log('showSearchResults', e))
return true return true
} else if (insideLink) { } else if (insideLink) {
let query = value.substring(start, end); let query = value.substring(start, end);
showSearchResults(titleSearch, query, input, value, 'link'); showSearchResults(titleSearch, query, input, value, 'link');
return true return true
} else { } else {
$('#link-complete').fadeOut(); $('#link-complete').hide();
} }
}) })
}) })
@ -634,20 +660,30 @@ function Editor(holder, input) {
let query = $input.val() let query = $input.val()
match(query, /{{query(!?):\s*([^}]+)}}/) match(query, /{{query([!@]?):\s*([^}]+)}}/)
.then(res => { .then(res => {
if (res[1] === '!') { if (res[1] === '@') {
return search.startQuery(res[2]) return search.startQuery(res[2], {internal: false})
.then(hits => _.flatMap(hits, formatLineWithoutTitleResult))
.then(results => editor.replaceChildren(id, results))
.catch(e => console.log('search query', e))
.finally(() => editor.render())
} else if (res[1] === '!') {
return search.startQuery(res[2], {internal: false})
.then(hits => _.uniqBy(_.flatMap(hits, formatTitleResult), _.property('text'))) .then(hits => _.uniqBy(_.flatMap(hits, formatTitleResult), _.property('text')))
.then(results => editor.replaceChildren(id, results)) .then(results => editor.replaceChildren(id, results))
.catch(e => console.log('search query', e))
.finally(() => editor.render()) .finally(() => editor.render())
} else { } else {
return search.startQuery(res[2]) return search.startQuery(res[2], {internal: false})
.then(hits => _.groupBy(hits, (it) => it.title)) .then(hits => _.groupBy(hits, (it) => it.title))
.then(hits => _.flatMap(hits, formatLineResult)) .then(hits => _.flatMap(hits, formatLineResult))
.then(results => editor.replaceChildren(id, results)) .then(results => editor.replaceChildren(id, results))
.catch(e => console.log('search query', e))
.finally(() => editor.render()) .finally(() => editor.render())
} }
}).catch(e => {
console.log(e)
}) })
}); });
return editor return editor
@ -658,7 +694,7 @@ function match(s, re) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let res = s.match(re) let res = s.match(re)
if (res) resolve(res) if (res) resolve(res)
else reject() else reject(s + ' did not match ' + re)
}); });
} }

View File

@ -1,4 +1,6 @@
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import * as chrono from "chrono-node";
import moment from "moment";
function createTitleSearch() { function createTitleSearch() {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
@ -29,6 +31,12 @@ function createTitleSearch() {
titleSearch: query => { titleSearch: query => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let search = titleFuse.search(query); let search = titleFuse.search(query);
let parseResult = chrono.nl.casual.parse(query)
if (parseResult.length) {
let m = moment(parseResult[0].start.date())
const title = m.format('LL')
search.unshift({item: {title, label: "Suggested page '" + title + "'"}})
}
search.unshift({item: {title: query, label: "Create page '" + query + "'"}}) search.unshift({item: {title: query, label: "Create page '" + query + "'"}})
search = search.slice(0, 25) search = search.slice(0, 25)
resolve(search) resolve(search)

View File

@ -1,67 +1,59 @@
// import {DataSet} from "vis-data/peer"; import {DataSet} from "vis-data/peer";
// import {Network} from "vis-network/peer"; import {Network} from "vis-network/peer";
import $ from 'jquery'; import $ from 'jquery';
function wikiGraph(selector, options) { function wikiGraph(selector, options) {
Promise.all([ $(selector).each(function (i, el) {
import("vis-data/peer"), let $el = $(el)
import("vis-network/peer"), var nodeName = $el.data('name')
]).then(imports => { fetch('/api/graph?name=' + nodeName)
const DataSet = imports[0].DataSet .then(res => res.json())
const Network = imports[1].Network .then(graph => {
var nodes = new DataSet(graph.nodes)
var edges = new DataSet(graph.edges)
$(selector).each(function (i, el) { var data = {
let $el = $(el) nodes: nodes,
var nodeName = $el.data('name') edges: edges
fetch('/api/graph?name=' + nodeName) };
.then(res => res.json())
.then(graph => {
var nodes = new DataSet(graph.nodes)
var edges = new DataSet(graph.edges)
var data = { var options = {
nodes: nodes, edges: {
edges: edges arrows: 'to',
}; color: {
highlight: 'green'
var options = { },
edges: { chosen: {
arrows: 'to', edge: function (values, id, selected, hovering) {
color: { if (this.from === 1) {
highlight: 'green' values.color = 'blue';
},
chosen: {
edge: function (values, id, selected, hovering) {
if (this.from === 1) {
values.color = 'blue';
}
} }
} }
},
nodes: {
shape: 'dot',
size: 15,
font: {
background: 'white'
}
},
layout: {
improvedLayout: true
} }
}; },
var network = new Network(el, data, options); nodes: {
network.on('doubleClick', function (props) { shape: 'dot',
if (props.nodes.length) { size: 15,
let nodeId = props.nodes[0] font: {
let node = nodes.get(nodeId) background: 'white'
// TODO: Reload directly in this same page
window.location.href = '/edit/' + node.label
} }
}) },
layout: {
improvedLayout: true
}
};
var network = new Network(el, data, options);
network.on('doubleClick', function (props) {
if (props.nodes.length) {
let nodeId = props.nodes[0]
let node = nodes.get(nodeId)
// TODO: Reload directly in this same page
window.location.href = '/edit/' + node.label
}
}) })
}) })
}) })
} }

View File

@ -3,16 +3,17 @@ import moment from 'moment'
import './styles.scss' import './styles.scss'
import Editor from './editor' import Editor from './editor'
import MD from './markdown' import MD from './markdown'
import wikiGraph from "./graph"; // import wikiGraph from "./graph";
import "./sr";
moment.locale('nl') moment.locale('nl')
// mermaid.initialize({startOnLoad: true}) // mermaid.initialize({startOnLoad: true})
// PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-mermaid') // PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-mermaid')
// PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-dot') // PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-dot')
$(function () { // $(function () {
setTimeout(() => wikiGraph('.graph-network'), 1); // setTimeout(() => wikiGraph('.graph-network'), 1);
}) // })
/* /*
* EVENTS * EVENTS
@ -34,7 +35,7 @@ $(document).on('keydown', '.keyboard-list', function (event) {
$(document).on('keydown', '#search-input', function (event) { $(document).on('keydown', '#search-input', function (event) {
let $ac = $('#autocomplete:visible'); let $ac = $('#autocomplete:visible');
if (event.key === 'Escape') { if (event.key === 'Escape') {
$(this).val(''); $(this).val('').removeClass('is-error');
if ($ac.length) { if ($ac.length) {
$ac.fadeOut(); $ac.fadeOut();
@ -95,3 +96,25 @@ $(document).on('click', '.calendar-update', function (event) {
}) })
return false return false
}) })
function activateTab(tab) {
console.log(tab)
$('.tabs .tab-page').toggleClass('tab-active', false);
$($(tab).data('target')).toggleClass('tab-active', true);
$('.tab-bar .tab').toggleClass('tab-active', false)
$(tab).toggleClass('tab-active', true)
}
$(document).on('click', '.tab', function () {
activateTab(this)
});
$(function () {
activateTab($('.tab-bar .tab-active')[0]);
$('.navbar-burger').on('click', function () {
let open = $(this).hasClass('is-active')
$('#' + $(this).data('target')).toggleClass('is-active', !open)
$(this).toggleClass('is-active', !open)
});
});

View File

@ -0,0 +1,56 @@
'use strict'
var util = require('util')
function Plugin(options) {
var self = function (md) {
self.options = options
self.init(md)
}
self.__proto__ = Plugin.prototype
self.id = 'markdown-tag'
return self
}
util.inherits(Plugin, Function)
Plugin.prototype.init = function (md) {
md.inline.ruler.push(this.id, this.parse.bind(this))
md.renderer.rules[this.id] = this.render.bind(this)
}
export function tagParser(id, state, silent) {
let input = state.src.slice(state.pos);
const match = /^#\S+/.exec(input)
if (!match) {
return false
}
console.log(match)
const prefixLength = match[0].length
if (!silent) {
console.log(state.src, state.pos, prefixLength)
let link = state.src.slice(state.pos + 1, state.pos + prefixLength)
let token = state.push(id, '', 0)
token.meta = {match: link, tag: true}
console.log(token)
}
state.pos += prefixLength
return true
}
Plugin.prototype.parse = function (state, silent) {
return tagParser(this.id, state, silent)
}
Plugin.prototype.render = function (tokens, id, options, env) {
let {match: link} = tokens[id].meta
return '<a href="' + this.options.relativeBaseURL + encodeURIComponent(link) + '" class="tag">' + '#' + link + '</a>';
}
export default (options) => {
return Plugin(options);
}

View File

@ -1,7 +1,9 @@
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
import MarkdownItWikilinks from "./wikilinks"; import MarkdownItWikilinks2 from "./wikilinks2";
import MarkdownItMetadata from "./metadatalinks";
import MarkdownItMark from "markdown-it-mark"; import MarkdownItMark from "markdown-it-mark";
// import MarkdownItKatex from "markdown-it-katex"; import MarkdownItKatex from "markdown-it-katex";
import MarkdownItTag from "./markdown-tag";
const MD = new MarkdownIt({ const MD = new MarkdownIt({
linkify: true, linkify: true,
@ -12,15 +14,26 @@ const MD = new MarkdownIt({
return ''; return '';
} }
}) })
MD.use(MarkdownItWikilinks2({
MD.use(MarkdownItWikilinks({
baseURL: document.querySelector('body').dataset.baseUrl, baseURL: document.querySelector('body').dataset.baseUrl,
uriSuffix: '', uriSuffix: '',
relativeBaseURL: '/edit/', relativeBaseURL: '/edit/',
htmlAttributes: { htmlAttributes: {
class: 'wiki-link' class: 'wiki-link'
}, },
})).use(MarkdownItMark) }))
// .use(MarkdownItKatex) MD.use(MarkdownItMetadata())
// MD.use(MarkdownItWikilinks({
// baseURL: document.querySelector('body').dataset.baseUrl,
// uriSuffix: '',
// relativeBaseURL: '/edit/',
// htmlAttributes: {
// class: 'wiki-link'
// },
// }))
MD.use(MarkdownItMark).use(MarkdownItKatex)
MD.use(MarkdownItTag({
relativeBaseURL: '/edit/',
}))
export default MD; export default MD;

View File

@ -1,6 +1,8 @@
import $ from 'jquery' import $ from 'jquery'
import 'jquery-contextmenu' import 'jquery-contextmenu'
import copy from 'copy-text-to-clipboard' import copy from 'copy-text-to-clipboard'
import axios from "axios";
import qs from "querystring";
function renderTree(tree) { function renderTree(tree) {
if (!tree) return [] if (!tree) return []
@ -22,7 +24,17 @@ function connectContextMenu(editor) {
createNewPage: { createNewPage: {
name: 'Create page from item', name: 'Create page from item',
callback: function (key, opt) { callback: function (key, opt) {
console.log('Create page from item', key, opt) console.log('Create page from item')
editor.flat(this, {base: true}).then(result => {
let data = {
'json': 1,
'p': result.title,
'summary': "",
'content': JSON.stringify(result.children),
};
console.log(data)
return axios.post('/save/', qs.encode(data))
}).then()
}, },
className: 'action-new-page' className: 'action-new-page'
}, },

View File

@ -0,0 +1,54 @@
'use strict'
var util = require('util')
function Plugin(options) {
var self = function (md) {
self.options = options
self.init(md)
}
self.__proto__ = Plugin.prototype
self.id = 'metadata'
return self
}
util.inherits(Plugin, Function)
Plugin.prototype.init = function (md) {
md.inline.ruler.before('text', this.id, this.parse.bind(this), {})
md.renderer.rules[this.id] = this.render.bind(this)
}
export function metadataLinkParser(id, state, silent) {
let input = state.src;
if (state.pos === 0 && input.match(/^([A-Za-z0-9 ]+)::/)) {
let [key, sep, value] = input.split(/::( |$)/)
console.log(key, value)
if (key.length === 0) return false;
if (!silent) {
let token = state.push(id, '', 0)
token.meta = {key, value, tag: 'metadata'}
}
state.pos = key.length + 3;
return true
}
return false
}
Plugin.prototype.parse = function (state, silent) {
return metadataLinkParser(this.id, state, silent)
}
Plugin.prototype.render = function (tokens, id, options, env) {
let {match: link, tag} = tokens[id].meta
if (tag === 'metadata') {
let {key, value} = tokens[id].meta
return '<span class="metadata-key"><a href="/edit/'+key.replace(/ +/g, '_')+'">'+key+'</a></span>: ';
}
}
export default (options) => {
return Plugin(options);
}

View File

@ -1,27 +1,55 @@
import $ from 'jquery' import $ from 'jquery'
import qs from 'querystring'; import qs from 'querystring'
import * as chrono from 'chrono-node'
import moment from 'moment';
function search(element) { function search(element) {
return { return {
element: element, element: element,
search(query) { search(query) {
element.classList.add('is-loading') element.classList.add('is-loading')
let result;
return startQuery(query) return startQuery(query)
.then(res => { .then(res => {
element.classList.remove('is-loading') element.classList.remove('is-loading', 'is-error')
result = res
return res return res
}).catch(e => {
console.log(e)
element.classList.add('is-error')
return result || []
}) })
} }
} }
} }
function startQuery(query) { function startQuery(query, opt) {
opt ||= {internal:true}
return fetch('/search/?' + qs.encode({q: query})) return fetch('/search/?' + qs.encode({q: query}))
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
let actualResult = []; let actualResult = [];
if (opt.internal) {
actualResult.push({
ref: query.replace(/\s+/g, '_'),
title: 'Create new page "' + query + '"',
line: 'New page',
text: 'New page',
})
let parseResult = chrono.nl.casual.parse(query)
if (parseResult.length) {
let m = moment(parseResult[0].start.date())
actualResult.push({
ref: m.format('LL').replace(/\s+/g, '_'),
title: m.format('LL'),
line: 'Suggested page',
text: 'Suggested page'
})
}
}
$.each(data.hits, (key, value) => { $.each(data.hits, (key, value) => {
actualResult.push({ actualResult.push({
id: value.id,
ref: value.fields.page, ref: value.fields.page,
title: value.fields.title, title: value.fields.title,
line: value.fields.text, line: value.fields.text,

135
editor/src/sr.js Normal file
View File

@ -0,0 +1,135 @@
import $ from 'jquery'
import search from "./search";
import qs from "querystring";
function fillResult($modal, result) {
$modal.data('block-id', result.id)
$modal.data('block-original-text', result.line)
let visibleText = result.line.replace(/\s*#\S+/g, '').trim();
$modal.data('block-text-without-tags', visibleText)
$modal.find('.block-title').show().text(result.title)
$modal.find('.block-text').show().val(visibleText)
}
async function replaceBlock(id, oldText) {
if (oldText === false) return false;
await fetch('/api/block/replace', {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: qs.encode({id, text: oldText}),
});
return true
}
function appendBlock(id, newText) {
return fetch('/api/block/append', {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: qs.encode({id, text: newText}),
});
}
function resetSRBox(text) {
return text + " #review #sr/1"
}
function processSRBox(review, originalText) {
let oldText = originalText
.replace(/#sr\/(\d+)/, function (text, srCount) {
if (review === 'never') return '';
let nextCount = 1;
const count = parseInt(srCount)
if (review === 'again') nextCount = 1
if (review === 'soon') nextCount = count
if (review === 'later') nextCount = count + 1
return '#sr/' + nextCount;
}).trim()
if (review === 'never') return oldText.replace(/#review/, '')
if (oldText === originalText) return false
return oldText
}
$(function () {
let reviewBlocks = [];
let isSR = false;
$('.start-review, .start-sr').on('click', function () {
isSR = $(this).hasClass('start-sr')
// Use different queries for different days
const query = isSR ? '+tag:sr tag:"sr/1" tag:"sr/2"' : '+tag:review tag:"sr/1"'
search.startQuery(query, {internal: false}).then((results) => {
reviewBlocks = results
$('.review-modal .end-of-review').hide();
$('.review-modal .review').show();
let $modal = $('.review-modal').first()
$modal.addClass('is-active')
if (reviewBlocks.length > 0) {
const first = reviewBlocks.shift()
fillResult($modal, first)
} else {
$('.review-modal .block-text').hide();
$('.review-modal .block-title').hide();
$('.review-modal .end-of-review').show();
$('.review-modal .review').hide();
}
})
return false
});
$('.modal-close, .delete, .close').on('click', function () {
$(this).parents('.modal').removeClass('is-active')
window.location.reload()
});
$('.modal .show-answer').on('click', function () {
$('.review-modal .block-answer').show();
});
$('.modal .review').on('click', function () {
const $modal = $(this).parents('.modal')
const review = $(this).data('review')
// NOTE: should we keep the review and sr/* tag in the editable text?
const originalText = $modal.data('block-original-text')
const originalTextWithoutTags = $modal.data('block-text-without-tags')
let text = $modal.find('.block-text').val()
let id = $modal.data('block-id')
processText(review, text, originalTextWithoutTags, originalText, id)
.then(() => {
if (reviewBlocks.length > 0) {
const first = reviewBlocks.shift()
// reload note with id
search.startQuery('id:' + first.id, {internal: false})
.then((results) => {
fillResult($modal, results[0])
})
} else {
$('.review-modal .block-text').hide();
$('.review-modal .block-title').hide();
$('.review-modal .end-of-review').show();
$('.review-modal .review').hide();
reviewBlocks = []
}
}).catch(e => console.log(e))
});
async function processText(review, text, originalTextWithoutTags, originalText, id) {
if (text !== originalTextWithoutTags) {
await appendBlock(id, resetSRBox(text))
}
await replaceBlock(id, processSRBox(review, originalText))
}
});

View File

@ -4,8 +4,13 @@
@import "~bulma/sass/base/_all"; @import "~bulma/sass/base/_all";
@import "~bulma/sass/elements/title.sass"; @import "~bulma/sass/elements/title.sass";
@import "~bulma/sass/elements/content.sass"; @import "~bulma/sass/elements/content.sass";
@import "~bulma/sass/elements/button.sass";
@import "~bulma/sass/form/shared.sass";
@import "~bulma/sass/form/input-textarea.sass";
@import "~bulma/sass/elements/other.sass";
@import "~bulma/sass/components/breadcrumb.sass"; @import "~bulma/sass/components/breadcrumb.sass";
@import "~bulma/sass/components/navbar.sass"; @import "~bulma/sass/components/navbar.sass";
@import "~bulma/sass/components/modal.sass";
@import '~jquery-contextmenu/dist/jquery.contextMenu.css'; @import '~jquery-contextmenu/dist/jquery.contextMenu.css';
//@import '~vis-network/styles/vis-network.css'; //@import '~vis-network/styles/vis-network.css';
@ -13,7 +18,7 @@
@import url('https://rsms.me/inter/inter.css'); @import url('https://rsms.me/inter/inter.css');
html { html {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
} }
body { body {
@ -29,6 +34,7 @@ body {
content: "[["; content: "[[";
color: #ccc; color: #ccc;
} }
.content a.wiki-link::after { .content a.wiki-link::after {
content: "]]"; content: "]]";
color: #ccc; color: #ccc;
@ -78,13 +84,10 @@ body {
display: none; display: none;
} }
.list-item .fold {
visibility: hidden;
}
.list-item:hover .fold { .list-item:hover .fold {
visibility: visible; visibility: visible;
} }
.list-item.selected .fold { .list-item.selected .fold {
visibility: visible; visibility: visible;
} }
@ -94,11 +97,17 @@ body {
} }
.list-item .fold { .list-item .fold {
height: 24px;
width: 24px;
text-align: center;
align-self: flex-start; align-self: flex-start;
flex-shrink: 0;
font-size: 10px; font-size: 10px;
margin-top: 4px;
margin-right: 3px; margin-right: 3px;
cursor: pointer; cursor: pointer;
visibility: hidden;
display: inline-block;
padding-top: 4px;
} }
// enable to close multi line content // enable to close multi line content
@ -194,15 +203,15 @@ mark {
outline: 4px solid #ffff99; outline: 4px solid #ffff99;
} }
.root mark::before { //.root mark::before {
content: "=="; // content: "==";
color: #999; // color: #999;
} //}
//
.root mark::after { //.root mark::after {
content: "=="; // content: "==";
color: #999; // color: #999;
} //}
.marker, .fold { .marker, .fold {
user-select: none; user-select: none;
@ -217,11 +226,12 @@ mark {
width: 100%; width: 100%;
.wiki-link { .wiki-link {
&::before, &::after { &::before, &::after {
content: ''; content: '';
} }
} }
} }
.sidebar-right { .sidebar-right {
width: 100%; width: 100%;
padding: 0 12px; padding: 0 12px;
@ -242,6 +252,7 @@ mark {
height: 32px; height: 32px;
text-align: center; text-align: center;
} }
.calendar-update:active { .calendar-update:active {
border: 3px solid #aaa; border: 3px solid #aaa;
border-radius: 4px; border-radius: 4px;
@ -276,12 +287,15 @@ mark {
margin-top: 6px; margin-top: 6px;
} }
} }
.week { .week {
background: #ebebff; background: #ebebff;
} }
.week a { .week a {
color: black; color: black;
} }
.day a { .day a {
position: absolute; position: absolute;
top: 0; top: 0;
@ -290,11 +304,13 @@ mark {
left: 0; left: 0;
color: black; color: black;
} }
.day .day-text { .day .day-text {
font-size: 9pt; font-size: 9pt;
display: block; display: block;
margin-top: 3px; margin-top: 3px;
} }
.day .day-count { .day .day-count {
position: absolute; position: absolute;
width: 100%; width: 100%;
@ -306,11 +322,14 @@ mark {
right: 0; right: 0;
bottom: 0; bottom: 0;
} }
.today { .today {
font-weight: bold; font-weight: bold;
} }
.current { .current {
background: #f0f0f0; background: #f0f0f0;
&.has-content::after { &.has-content::after {
font-weight: bold; font-weight: bold;
content: '\00B7'; content: '\00B7';
@ -326,16 +345,19 @@ mark {
.footer { .footer {
padding: 0 12px; padding: 0 12px;
margin-top: 300px;
} }
.wiki-list-editor { .wiki-list-editor {
max-width: 960px; max-width: 960px;
} }
.wiki-list-editor { .wiki-list-editor {
.table-wrapper { .table-wrapper {
max-width: 960px; max-width: 960px;
overflow-x: auto; overflow-x: auto;
} }
.table-wrapper td { .table-wrapper td {
white-space: pre; white-space: pre;
text-wrap: none; text-wrap: none;
@ -350,6 +372,7 @@ mark {
flex-grow: 1; flex-grow: 1;
min-height: 24px; min-height: 24px;
} }
#autocomplete { #autocomplete {
z-index: 1; z-index: 1;
width: 640px; width: 640px;
@ -360,25 +383,31 @@ mark {
background: white; background: white;
border: 1px solid #ccc; border: 1px solid #ccc;
} }
#autocomplete li > a { #autocomplete li > a {
display: block; display: block;
} }
#autocomplete li div { #autocomplete li div {
font-size: 0.8em; font-size: 0.8em;
display: block; display: block;
color: black; color: black;
} }
#autocomplete li { #autocomplete li {
padding: 4px 16px; padding: 4px 16px;
max-height: 5em; max-height: 5em;
overflow: hidden; overflow: hidden;
} }
#autocomplete li:hover { #autocomplete li:hover {
background: #fefefe; background: #fefefe;
} }
#autocomplete li.selected { #autocomplete li.selected {
background: lightblue; background: lightblue;
} }
#link-complete { #link-complete {
z-index: 1; z-index: 1;
width: 280px; width: 280px;
@ -395,6 +424,7 @@ mark {
padding: 4px 16px; padding: 4px 16px;
white-space: nowrap; white-space: nowrap;
} }
#link-complete li.selected { #link-complete li.selected {
background: lightblue; background: lightblue;
} }
@ -431,6 +461,7 @@ ins {
.checklist--item-text { .checklist--item-text {
align-self: center; align-self: center;
} }
html { html {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
} }
@ -458,14 +489,26 @@ input.input-line {
align-items: center; align-items: center;
} }
textarea { textarea {
border: none; border: none;
resize: none; resize: none;
} }
.list-item .content { .list-item .content {
white-space: pre-wrap; //white-space: pre-wrap;
}
.wiki-list-editor .content {
blockquote {
padding-top: 0;
padding-bottom: 0;
}
h1, h2, h3, h4, h5, h6 {
margin-bottom: 0;
}
ul, ol {
margin-top: 0;
}
} }
.hide { .hide {
@ -475,15 +518,19 @@ textarea {
.selected { .selected {
background: lightblue; background: lightblue;
} }
.fold.closed + .marker { .fold.closed + .marker {
border-color: lightblue; border-color: lightblue;
} }
.selected .marker { .selected .marker {
border-color: lightblue; border-color: lightblue;
} }
#editor { #editor {
width: 750px; width: 750px;
} }
.editor.selected .marker { .editor.selected .marker {
/*border-color: white;*/ /*border-color: white;*/
} }
@ -540,11 +587,13 @@ input.input-line, input.input-line:active {
.breadcrumb li { .breadcrumb li {
max-width: 200px; max-width: 200px;
} }
.breadcrumb li > a { .breadcrumb li > a {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
display: block; display: block;
} }
.searchbar { .searchbar {
position: relative; position: relative;
display: flex; display: flex;
@ -558,14 +607,78 @@ input.input-line, input.input-line:active {
width: 400px; width: 400px;
} }
} }
.wiki-table .expression { .wiki-table .expression {
display: none; display: none;
} }
.todo--todo { .todo--todo {
} }
.todo--done { .todo--done {
text-decoration: line-through; text-decoration: line-through;
text-decoration-skip: leading-spaces; text-decoration-skip: leading-spaces;
color: #999; color: #999;
} }
.tabs {
width: 100%;
}
.tab-bar {
display: flex;
border-bottom: 1px solid black;
}
.tab-bar .tab {
padding: 8px 16px;
cursor: pointer;
border-bottom: 3px solid white;
}
.tab-bar .tab:first-child {
margin-left: 24px;
}
.tab-bar .tab.tab-active {
border-bottom: 3px solid black;
}
.tab-bar .tab + .tab {
margin-left: 24px;
}
.tab-page {
display: none;
}
.tab.tab-active {
}
.tab-page.tab-active {
display: block;
}
.search.input {
border: none;
padding: 2px;
}
.search.input.is-error {
outline: red solid 4px;
}
.tag {
background: #deeeee;
color: #444;
border-radius: 3px;
padding: 2px 4px;
}
.backrefs .tag {
background: white;
}
.review {
}

View File

@ -2,7 +2,26 @@
import Plugin from "markdown-it-regexp"; import Plugin from "markdown-it-regexp";
import extend from "extend"; import extend from "extend";
import sanitize from "sanitize-filename";
var illegalRe = /[\/\?<>\\\*\|"]/g;
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
var reservedRe = /^\.+$/;
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
var windowsTrailingRe = /[\. ]+$/;
function sanitize(input) {
const replacement = '';
if (typeof input !== 'string') {
throw new Error('Input must be string');
}
var sanitized = input
.replace(illegalRe, replacement)
.replace(controlRe, replacement)
.replace(reservedRe, replacement)
.replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement);
return sanitized;
}
export default (options) => { export default (options) => {
const defaults = { const defaults = {
@ -17,7 +36,7 @@ export default (options) => {
}, },
postProcessPageName: (pageName) => { postProcessPageName: (pageName) => {
pageName = pageName.trim() pageName = pageName.trim()
pageName = pageName.split('/').map(sanitize).join('/') pageName = pageName.split('/').map(sanitize).map(sanitize).join('/')
pageName = pageName.replace(/\s+/g, '_') pageName = pageName.replace(/\s+/g, '_')
return pageName return pageName
}, },

82
editor/src/wikilinks2.js Normal file
View File

@ -0,0 +1,82 @@
'use strict'
var util = require('util')
function Plugin(options) {
var self = function (md) {
self.options = options
self.init(md)
}
self.__proto__ = Plugin.prototype
self.id = 'wikilinks'
return self
}
util.inherits(Plugin, Function)
Plugin.prototype.init = function (md) {
md.inline.ruler.push(this.id, this.parse.bind(this))
md.renderer.rules[this.id] = this.render.bind(this)
}
export function linkParser(id, state, silent) {
let input = state.src.slice(state.pos);
const match = /^#?\[\[/.exec(input)
if (!match) {
return false
}
let prefixLength = match[0].length
let p = state.pos + prefixLength
let open = 2
while (p < state.src.length - 1 && open > 0) {
if (state.src[p] === '[' && state.src[p + 1] === '[') {
open += 2
p += 2
} else if (state.src[p] === ']' && state.src[p + 1] === ']') {
open -= 2
p += 2
} else {
p++
}
}
if (open === 0) {
if (!silent) {
let link = state.src.slice(state.pos + prefixLength, p - 2)
let token = state.push(id, '', 0)
token.meta = {match: link, tag: prefixLength === 3}
}
state.pos = p
return true
}
return false
}
Plugin.prototype.parse = function (state, silent) {
return linkParser(this.id, state, silent)
}
Plugin.prototype.render = function (tokens, id, options, env) {
let {match: link, tag} = tokens[id].meta
if (tag) {
if (tag === 'metadata') {
let {key, value} = tokens[id].meta
return '<span class="metadata-key">'+key+'</span>: '+value;
}
if (link === 'TODO') {
return '<input type="checkbox" class="checkbox">';
} else if (link === 'DONE') {
return '<input type="checkbox" class="checkbox" checked>';
}
}
return '<a href="'+this.options.relativeBaseURL+encodeURIComponent(link.replace(/ +/g, '_')) + '" class="wiki-link">' + (tag ? '#' : '') + link + '</a>';
}
export default (options) => {
return Plugin(options);
}

View File

@ -0,0 +1,70 @@
/*
* Wiki - A wiki with editor
* Copyright (c) 2021 Peter Stuifzand
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import assert from 'assert'
import MarkdownIt from "markdown-it";
import MarkdownItWikilinks2 from "../src/wikilinks2";
const MD = new MarkdownIt({
linkify: false,
highlight: function (str, lang) {
if (lang === 'mermaid') {
return '<div class="mermaid">' + str + '</div>';
}
return '';
}
})
MD.use(MarkdownItWikilinks2({
baseURL: 'http://localhost/',
uriSuffix: '',
relativeBaseURL: '/edit/',
htmlAttributes: {
class: 'wiki-link'
},
}))
describe('MD', function () {
it('parseLinks', function () {
assert.deepStrictEqual(MD.renderInline("#[[TODO]]"), '<input type="checkbox" class="checkbox">')
assert.deepStrictEqual(MD.renderInline("#[[TODO]] #[[DONE]]"), '<input type="checkbox" class="checkbox"> <input type="checkbox" class="checkbox" checked>')
})
it('parseLinks 2', function () {
assert.deepStrictEqual(MD.renderInline("#[[TODO]] #[[DONE]]"), '<input type="checkbox" class="checkbox"> <input type="checkbox" class="checkbox" checked>')
})
it('parseLinks 3', function () {
assert.deepStrictEqual(MD.renderInline("test #[[TODO]] test2"), 'test <input type="checkbox" class="checkbox"> test2')
})
it('parseLinks 4', function () {
assert.deepStrictEqual(MD.renderInline("test [[test]] [[test2]] [[test3]]"), 'test <a href="/edit/test" class="wiki-link">test</a> <a href="/edit/test2" class="wiki-link">test2</a> <a href="/edit/test3" class="wiki-link">test3</a>')
})
it('parseLinks 5', function () {
assert.deepStrictEqual(MD.renderInline("test [[test]]"), 'test <a href="/edit/test" class="wiki-link">test</a>')
})
it('parseLinks 6', function () {
assert.deepStrictEqual(MD.renderInline("test [[test]] [[test2]]"), 'test <a href="/edit/test" class="wiki-link">test</a> <a href="/edit/test2" class="wiki-link">test2</a>')
})
it('parseLinks tag', function () {
assert.deepStrictEqual(MD.renderInline("test #[[test]]"), 'test <a href="/edit/test" class="wiki-link">#test</a>')
})
it('parseLinks double url', function () {
assert.deepStrictEqual(MD.renderInline("[[test [[link]] test2]]"), '<a href="/edit/test_%5B%5Blink%5D%5D_test2" class="wiki-link">test [[link]] test2</a>')
})
})

View File

@ -0,0 +1,137 @@
/*
* Wiki - A wiki with editor
* Copyright (c) 2021 Peter Stuifzand
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import assert from 'assert'
import {linkParser} from '../src/wikilinks2'
describe('linkParser', function () {
it('parse', function () {
let state = {src: '', pos: 0, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), false);
assert.deepStrictEqual(state, {
src: '',
pos: 0,
tokens: []
})
})
it('parse 2', function () {
let state = {src: '[[Link]]', pos: 0, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '[[Link]]',
pos: 8,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'Link', tag: false}
}]
})
})
it('parse 3', function () {
let state = {src: '1234[[Link]]', pos: 4, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '1234[[Link]]',
pos: 12,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'Link', tag: false}
}]
})
})
it('parse 4', function () {
let state = {src: '1234#[[TODO]]', pos: 4, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '1234#[[TODO]]',
pos: 13,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'TODO', tag: true}
}]
})
})
it('parse text and two links', function () {
let state = {src: '1234 [[Link]] [[Link2]]', pos: 5, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '1234 [[Link]] [[Link2]]',
pos: 13,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'Link', tag: false}
}]
})
})
it('parse text and two links', function () {
let state = {src: '1234 [[hello [[world]] Link2]]', pos: 5, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '1234 [[hello [[world]] Link2]]',
pos: 30,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'hello [[world]] Link2', tag: false}
}]
})
})
})

146
file.go
View File

@ -33,13 +33,14 @@ import (
"strings" "strings"
"time" "time"
"github.com/blevesearch/bleve" "github.com/blevesearch/bleve/v2"
"github.com/sergi/go-diff/diffmatchpatch" "github.com/sergi/go-diff/diffmatchpatch"
) )
const ( const (
DocumentsFile = "_documents.json" DocumentsFile = "_documents.json"
LinksFile = "_links.json" LinksFile = "_links.json"
BlocksDirectory = "_blocks"
) )
var BlockNotFound = errors.New("block not found") var BlockNotFound = errors.New("block not found")
@ -51,6 +52,12 @@ type saveMessage struct {
author string author string
} }
type Block struct {
Text string
Children []string
Parent string
}
// ListItemV2 is way to convert from old structure to new structure // ListItemV2 is way to convert from old structure to new structure
type ListItemV2 struct { type ListItemV2 struct {
ID ID ID ID
@ -88,8 +95,6 @@ type FilePages struct {
dirname string dirname string
saveC chan saveMessage saveC chan saveMessage
index bleve.Index index bleve.Index
blockRepo BlockRepository
} }
type BlockResponse struct { type BlockResponse struct {
@ -100,18 +105,13 @@ type BlockResponse struct {
Parents []string Parents []string
} }
func NewFilePages(dirname string, index bleve.Index) *FilePages { func NewFilePages(dirname string, index bleve.Index) PagesRepository {
blockRepo, err := NewBlockRepo(dirname) err := os.MkdirAll(filepath.Join(dirname, "_blocks"), 0777)
if err != nil { if err != nil {
log.Fatal(err) log.Fatalln(err)
} }
fp := &FilePages{ fp := &FilePages{dirname, make(chan saveMessage), index}
dirname: dirname,
saveC: make(chan saveMessage),
index: index,
blockRepo: blockRepo,
}
go func() { go func() {
for msg := range fp.saveC { for msg := range fp.saveC {
@ -174,7 +174,7 @@ func (fp *FilePages) Get(name string) Page {
} }
for _, name := range names { for _, name := range names {
blocks, err := fp.blockRepo.GetBlocks(name) blocks, err := loadBlocks(fp.dirname, name)
if err != nil && errors.Is(err, BlockNotFound) { if err != nil && errors.Is(err, BlockNotFound) {
continue continue
} }
@ -289,7 +289,7 @@ func (fp *FilePages) save(msg saveMessage) error {
} }
sw.Start("create blocks") sw.Start("create blocks")
err := fp.saveBlocksFromPage(fp.dirname, page) err := saveBlocksFromPage(fp.dirname, page)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
@ -333,18 +333,23 @@ func (fp *FilePages) save(msg saveMessage) error {
sw.Stop() sw.Stop()
sw.Start("index") sw.Start("index")
searchObjects, err := createSearchObjects(fp, page.Name) searchObjects, err := createSearchObjects(page.Name)
if err != nil { if err != nil {
return fmt.Errorf("while creating search object %s: %w", page.Name, err) return fmt.Errorf("while creating search object %s: %w", page.Name, err)
} }
batch := fp.index.NewBatch()
for _, so := range searchObjects { for _, so := range searchObjects {
if fp.index != nil { if fp.index != nil {
err = fp.index.Index(so.ID, so) err = batch.Index(so.ID, so)
if err != nil { if err != nil {
return fmt.Errorf("while indexing %s: %w", page.Name, err) return fmt.Errorf("while indexing %s: %w", page.Name, err)
} }
} }
} }
err = fp.index.Batch(batch)
if err != nil {
return fmt.Errorf("while indexing %s: %w", page.Name, err)
}
sw.Stop() sw.Stop()
sw.Start("links") sw.Start("links")
err = saveLinksIncremental(fp.dirname, page.Title) err = saveLinksIncremental(fp.dirname, page.Title)
@ -368,7 +373,7 @@ func saveWithNewIDs(dirname string, listItems []ListItemV2, pageName string) ([]
return newListItems, nil return newListItems, nil
} }
func (fp* FilePages) saveBlocksFromPage(dirname string, page Page) error { func saveBlocksFromPage(dirname string, page Page) error {
log.Printf("Processing: %q\n", page.Name) log.Printf("Processing: %q\n", page.Name)
var listItems []ListItem var listItems []ListItem
err := json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems) err := json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
@ -386,7 +391,7 @@ func (fp* FilePages) saveBlocksFromPage(dirname string, page Page) error {
prevList := make(map[string]ListItem) prevList := make(map[string]ListItem)
root := "root" root := "root"
parentBlock, err := fp.blockRepo.GetBlock(page.Name) parentBlock, err := loadBlock(dirname, page.Name)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} else { } else {
@ -446,7 +451,7 @@ func (fp* FilePages) saveBlocksFromPage(dirname string, page Page) error {
prev = &listItems[i] prev = &listItems[i]
} }
// TODO: find out if this is still necessary // TODO: found out if this is still necessary
log.Printf("Loading parent block: %q", rootParentID) log.Printf("Loading parent block: %q", rootParentID)
f, err := os.Open(filepath.Join(dirname, BlocksDirectory, rootParentID)) f, err := os.Open(filepath.Join(dirname, BlocksDirectory, rootParentID))
if err == nil { if err == nil {
@ -467,15 +472,101 @@ func (fp* FilePages) saveBlocksFromPage(dirname string, page Page) error {
} }
for id, block := range blocks { for id, block := range blocks {
err := fp.blockRepo.SaveBlock(id, block) log.Printf("Writing to %q\n", id)
f, err := os.OpenFile(filepath.Join(dirname, BlocksDirectory, id), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
log.Println(err)
continue
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(&block)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
f.Close()
} }
return err return err
} }
func loadBlocks(dirname, rootBlockID string) (BlockResponse, error) {
resp := BlockResponse{
rootBlockID,
"",
nil,
nil,
nil,
}
resp.Texts = make(map[string]string)
resp.Children = make(map[string][]string)
queue := []string{rootBlockID}
block, err := loadBlock(dirname, rootBlockID)
if err != nil {
return BlockResponse{}, err
}
// NOTE: what does this do?
if rootBlockID[0] != '_' && block.Children == nil {
return BlockResponse{}, fmt.Errorf("not a block and has no children: %w", BlockNotFound)
}
prevID := rootBlockID
parentID := block.Parent
for parentID != "root" {
parent, err := loadBlock(dirname, parentID)
if err != nil {
return BlockResponse{}, fmt.Errorf("while loading current parent block (%s->%s): %w", prevID, parentID, err)
}
resp.Texts[parentID] = parent.Text
resp.Children[parentID] = parent.Children
resp.ParentID = parentID
resp.Parents = append(resp.Parents, parentID)
prevID = parentID
parentID = parent.Parent
}
if parentID == "root" {
resp.ParentID = "root"
}
for {
if len(queue) == 0 {
break
}
current := queue[0]
queue = queue[1:]
block, err := loadBlock(dirname, current)
if err != nil {
return BlockResponse{}, err
}
resp.Texts[current] = block.Text
resp.Children[current] = block.Children
queue = append(queue, block.Children...)
}
return resp, nil
}
func loadBlock(dirname, blockID string) (Block, error) {
f, err := os.Open(filepath.Join(dirname, BlocksDirectory, blockID))
if err != nil {
return Block{}, fmt.Errorf("%q: %w", blockID, BlockNotFound)
}
defer f.Close()
var block Block
err = json.NewDecoder(f).Decode(&block)
if err != nil {
return Block{}, fmt.Errorf("%q: %v", blockID, err)
}
return block, nil
}
func saveLinksIncremental(dirname, title string) error { func saveLinksIncremental(dirname, title string) error {
type Document struct { type Document struct {
Title string `json:"title"` Title string `json:"title"`
@ -500,7 +591,7 @@ func saveLinksIncremental(dirname, title string) error {
titles[title] = true titles[title] = true
results = nil results = nil
for t, _ := range titles { for t := range titles {
results = append(results, Document{t}) results = append(results, Document{t})
} }
@ -613,6 +704,9 @@ func (fp *FilePages) PageHistory(p string) ([]Revision, error) {
commitId := line[0:start] commitId := line[0:start]
rest := line[start+1:] rest := line[start+1:]
pageText := gitRevision(fp.dirname, page, commitId) pageText := gitRevision(fp.dirname, page, commitId)
if pageText == "" {
return nil, errors.New("git revision failed")
}
revisions = append(revisions, Revision{ revisions = append(revisions, Revision{
Version: commitId, Version: commitId,
Page: DiffPage{Content: pageText}, Page: DiffPage{Content: pageText},
@ -641,8 +735,14 @@ func gitRevision(dirname, page, version string) string {
cmd.Dir = dirname cmd.Dir = dirname
buf := bytes.Buffer{} buf := bytes.Buffer{}
cmd.Stdout = &buf cmd.Stdout = &buf
cmd.Start() err := cmd.Start()
cmd.Wait() if err != nil {
return ""
}
err = cmd.Wait()
if err != nil {
return ""
}
return buf.String() return buf.String()
} }

7
go.mod
View File

@ -3,16 +3,16 @@ module p83.nl/go/wiki
go 1.14 go 1.14
require ( require (
github.com/RoaringBitmap/roaring v0.4.23 // indirect
github.com/blevesearch/bleve v1.0.9 github.com/blevesearch/bleve v1.0.9
github.com/blevesearch/bleve/v2 v2.3.0
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a // indirect
github.com/golang/protobuf v1.4.2 // indirect github.com/golang/protobuf v1.4.2 // indirect
github.com/iancoleman/strcase v0.1.2 github.com/iancoleman/strcase v0.1.2
github.com/jmhodges/levigo v1.0.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect
@ -20,12 +20,9 @@ require (
github.com/sergi/go-diff v1.1.0 github.com/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
github.com/tinylib/msgp v1.1.2 // indirect
github.com/yuin/goldmark v1.1.32 github.com/yuin/goldmark v1.1.32
go.etcd.io/bbolt v1.3.5 // indirect
golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/protobuf v1.25.0 // indirect google.golang.org/protobuf v1.25.0 // indirect
p83.nl/go/ekster v0.0.0-20191119211024-4511657daa0b p83.nl/go/ekster v0.0.0-20191119211024-4511657daa0b
p83.nl/go/indieauth v0.1.0 p83.nl/go/indieauth v0.1.0

42
go.sum
View File

@ -2,22 +2,35 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhINmlHxKeo= github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/blevesearch/bleve v1.0.9 h1:kqw/Ank/61UV9/Bx9kCcnfH6qWPgmS8O5LNfpsgzASg= github.com/blevesearch/bleve v1.0.9 h1:kqw/Ank/61UV9/Bx9kCcnfH6qWPgmS8O5LNfpsgzASg=
github.com/blevesearch/bleve v1.0.9/go.mod h1:tb04/rbU29clbtNgorgFd8XdJea4x3ybYaOjWKr+UBU= github.com/blevesearch/bleve v1.0.9/go.mod h1:tb04/rbU29clbtNgorgFd8XdJea4x3ybYaOjWKr+UBU=
github.com/blevesearch/bleve/v2 v2.3.0 h1:5XKlSdpcjeJdE7n0FUEDeJRJwLuhPxq+k5n7h5UaJkg=
github.com/blevesearch/bleve/v2 v2.3.0/go.mod h1:egW/6gZEhM3oBvRjuHXGvGb92cKZ9867OqPZAmCG8MQ=
github.com/blevesearch/bleve_index_api v1.0.1 h1:nx9++0hnyiGOHJwQQYfsUGzpRdEVE5LsylmmngQvaFk=
github.com/blevesearch/bleve_index_api v1.0.1/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040 h1:SjYVcfJVZoCfBlg+fkaq2eoZHTf5HaJfaTeTkOtyfHQ= github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040 h1:SjYVcfJVZoCfBlg+fkaq2eoZHTf5HaJfaTeTkOtyfHQ=
github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040/go.mod h1:WH+MU2F4T0VmSdaPX+Wu5GYoZBrYWdOZWSjzvYcDmqQ= github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040/go.mod h1:WH+MU2F4T0VmSdaPX+Wu5GYoZBrYWdOZWSjzvYcDmqQ=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/mmap-go v1.0.2 h1:JtMHb+FgQCTTYIhtMvimw15dJwu1Y5lrZDMOFXVWPk0=
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
github.com/blevesearch/mmap-go v1.0.3 h1:7QkALgFNooSq3a46AE+pWeKASAZc9SiNFJhDGF1NDx4=
github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs=
github.com/blevesearch/scorch_segment_api/v2 v2.1.0 h1:NFwteOpZEvJk5Vg0H6gD0hxupsG3JYocE4DBvsA2GZI=
github.com/blevesearch/scorch_segment_api/v2 v2.1.0/go.mod h1:uch7xyyO/Alxkuxa+CGs79vw0QY8BENSBjg6Mw5L5DE=
github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac= github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac=
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.1 h1:1SYRwyoFLwG3sj0ed89RLtM15amfX2pXlYbFOnF8zNU=
github.com/blevesearch/upsidedown_store_api v1.0.1/go.mod h1:MQDVGpHZrpe3Uy26zJBf/a8h0FZY6xJbthIMm8myH2Q=
github.com/blevesearch/vellum v1.0.7 h1:+vn8rfyCRHxKVRgDLeR0FAXej2+6mEb5Q15aQE/XESQ=
github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE=
github.com/blevesearch/zap/v11 v11.0.9 h1:wlSrDBeGN1G4M51NQHIXca23ttwUfQpWaK7uhO5lRSo= github.com/blevesearch/zap/v11 v11.0.9 h1:wlSrDBeGN1G4M51NQHIXca23ttwUfQpWaK7uhO5lRSo=
github.com/blevesearch/zap/v11 v11.0.9/go.mod h1:47hzinvmY2EvvJruzsSCJpro7so8L1neseaGjrtXHOY= github.com/blevesearch/zap/v11 v11.0.9/go.mod h1:47hzinvmY2EvvJruzsSCJpro7so8L1neseaGjrtXHOY=
github.com/blevesearch/zap/v12 v12.0.9 h1:PpatkY+BLVFZf0Ok3/fwgI/I4RU0z5blXFGuQANmqXk= github.com/blevesearch/zap/v12 v12.0.9 h1:PpatkY+BLVFZf0Ok3/fwgI/I4RU0z5blXFGuQANmqXk=
@ -26,6 +39,16 @@ github.com/blevesearch/zap/v13 v13.0.1 h1:NSCM6uKu77Vn/x9nlPp4pE1o/bftqcOWZEHSyZ
github.com/blevesearch/zap/v13 v13.0.1/go.mod h1:XmyNLMvMf8Z5FjLANXwUeDW3e1+o77TTGUWrth7T9WI= github.com/blevesearch/zap/v13 v13.0.1/go.mod h1:XmyNLMvMf8Z5FjLANXwUeDW3e1+o77TTGUWrth7T9WI=
github.com/blevesearch/zap/v14 v14.0.0 h1:HF8Ysjm13qxB0jTGaKLlatNXmJbQD8bY+PrPxm5v4hE= github.com/blevesearch/zap/v14 v14.0.0 h1:HF8Ysjm13qxB0jTGaKLlatNXmJbQD8bY+PrPxm5v4hE=
github.com/blevesearch/zap/v14 v14.0.0/go.mod h1:sUc/gPGJlFbSQ2ZUh/wGRYwkKx+Dg/5p+dd+eq6QMXk= github.com/blevesearch/zap/v14 v14.0.0/go.mod h1:sUc/gPGJlFbSQ2ZUh/wGRYwkKx+Dg/5p+dd+eq6QMXk=
github.com/blevesearch/zapx/v11 v11.3.2 h1:TDdcbaA0Yz3Y5zpTrpvyW1AeicqWTJL3g8D5g48RiHM=
github.com/blevesearch/zapx/v11 v11.3.2/go.mod h1:YzTfUm4kS3e8OmTXDHVV8OzC5MWPO/VPJZQgPNVb4Lc=
github.com/blevesearch/zapx/v12 v12.3.2 h1:XB09XMg/3ibeIJRCm2zjkaVwrtAuk6c55YRSmVlwUDk=
github.com/blevesearch/zapx/v12 v12.3.2/go.mod h1:RMl6lOZqF+sTxKvhQDJ5yK2LT3Mu7E2p/jGdjAaiRxs=
github.com/blevesearch/zapx/v13 v13.3.2 h1:mTvALh6oayreac07VRAv94FLvTHeSBM9sZ1gmVt0N2k=
github.com/blevesearch/zapx/v13 v13.3.2/go.mod h1:eppobNM35U4C22yDvTuxV9xPqo10pwfP/jugL4INWG4=
github.com/blevesearch/zapx/v14 v14.3.2 h1:oW36JVaZDzrzmBa1X5jdTIYzdhkOQnr/ie13Cb2X7MQ=
github.com/blevesearch/zapx/v14 v14.3.2/go.mod h1:zXNcVzukh0AvG57oUtT1T0ndi09H0kELNaNmekEy0jw=
github.com/blevesearch/zapx/v15 v15.3.2 h1:OZNE4CQ9hQhnB21ySC7x2/9Q35U3WtRXLAh5L2gdCXc=
github.com/blevesearch/zapx/v15 v15.3.2/go.mod h1:C+f/97ZzTzK6vt/7sVlZdzZxKu+5+j4SrGCvr9dJzaY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -33,6 +56,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
github.com/couchbase/vellum v1.0.1 h1:qrj9ohvZedvc51S5KzPfJ6P6z0Vqzv7Lx7k3mVc2WOk= github.com/couchbase/vellum v1.0.1 h1:qrj9ohvZedvc51S5KzPfJ6P6z0Vqzv7Lx7k3mVc2WOk=
github.com/couchbase/vellum v1.0.1/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4= github.com/couchbase/vellum v1.0.1/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
@ -57,9 +81,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a h1:FQqoVvjbiUioBBFUL5up+h+GdCa/AnJsL/1bIs/veSI=
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -82,7 +103,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
@ -92,7 +112,6 @@ github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5N
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U=
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw= github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -114,7 +133,6 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -142,8 +160,6 @@ github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpP
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok=
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
@ -188,8 +204,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"log"
"strings" "strings"
) )
@ -76,6 +77,24 @@ func formatTitle(w io.Writer, input string, root Tree, indent int) {
} }
} }
func findAllLinks(input string, root Tree) []string {
var links []string
typ := root.cur.typ
if typ == "link" {
links = append(links, root.children[1].text(input))
}
for _, c := range root.children {
links = append(links, findAllLinks(input, c)...)
}
return links
}
func FindAllLinks(input string) []string {
var p Parser
root := p.Parse(input)
return findAllLinks(input, root)
}
func FormatHtmlTitle(input string) template.HTML { func FormatHtmlTitle(input string) template.HTML {
p := Parser{} p := Parser{}
root := p.Parse(input) root := p.Parse(input)
@ -87,13 +106,16 @@ func FormatHtmlTitle(input string) template.HTML {
func (p *Parser) Parse(input string) Tree { func (p *Parser) Parse(input string) Tree {
p.stack = append(p.stack, Tree{}) p.stack = append(p.stack, Tree{})
limit := 1000
i := 0 i := 0
p.pushMarker(i) p.pushMarker(i)
for i < len(input) { for i < len(input) && limit > 0 {
p.pushMarker(i) p.pushMarker(i)
for i < len(input) && (input[i] != '[' && input[i] != ']') { for i < len(input) && (input[i] != '[' && input[i] != ']') {
i++ i++
limit--
} }
p.popMarker(i, "text") p.popMarker(i, "text")
if i+2 <= len(input) && input[i:i+2] == "[[" { if i+2 <= len(input) && input[i:i+2] == "[[" {
@ -109,8 +131,13 @@ func (p *Parser) Parse(input string) Tree {
p.popMarker(i, "end link tag") p.popMarker(i, "end link tag")
p.popMarker(i, "link") p.popMarker(i, "link")
} }
limit--
} }
p.popMarker(i, "full text") p.popMarker(i, "full text")
if limit == 0 {
log.Println("LIMIT REACHED: ", input)
}
return p.output() return p.output()
} }

View File

@ -25,15 +25,31 @@ import (
) )
func TestFormatHtmlTitle(t *testing.T) { func TestFormatHtmlTitle(t *testing.T) {
tests := []struct { tests := []struct {
input, output string input, output string
}{ }{
{input: "hello", output: "hello"}, {input: "hello", output: "hello"},
{input: "hello [[world]]", output: `hello <a href="world">[[world]]</a>`}, {input: "hello [[world]]", output: `hello <a href="world">[[world]]</a>`},
{input: "hello [[world]] end", output: `hello <a href="world">[[world]]</a> end`},
{input: "hello [[world [[current stuff]] here]] end", output: `hello <a href="world [[current stuff]] here">[[world [[current stuff]] here]]</a> end`},
} }
for _, test := range tests { for _, test := range tests {
s := FormatHtmlTitle(test.input) s := FormatHtmlTitle(test.input)
assert.Equal(t, test.output, string(s)) assert.Equal(t, test.output, string(s))
} }
} }
func TestFindAllLinks(t *testing.T) {
tests := []struct {
input string
output []string
}{
{input: "hello", output: nil},
{input: "hello [[world]]", output: []string{"world"}},
{input: "hello [[world]] end", output: []string{"world"}},
{input: "hello [[world [[current stuff]] here]] end", output: []string{"world [[current stuff]] here", "current stuff"}},
}
for _, test := range tests {
links := FindAllLinks(test.input)
assert.Equal(t, test.output, links)
}
}

View File

@ -6,7 +6,8 @@ import textareaAutosizeInit from "./textarea.autosize"
import createCursor from './cursor' import createCursor from './cursor'
import Store from './store' import Store from './store'
import Keymap from './keymap' import Keymap from './keymap'
import getCaretCoordinates from "../editor/src/caret-position"; import getCaretCoordinates from "../editor/src/caret-position"
import TurndownService from "turndown"
textareaAutosizeInit($) textareaAutosizeInit($)
@ -73,8 +74,10 @@ function editor(root, inputData, options) {
function save() { function save() {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
if (store.hasChanged()) { if (store.hasChanged()) {
resolve(store.debug().result) let result = store.debug().result
resolve(result)
store.clearChanged() store.clearChanged()
} }
}); });
@ -109,12 +112,13 @@ function editor(root, inputData, options) {
}); });
} }
function flat(element) { function flat(element, opt) {
opt = opt || {}
let item = $(element).parents('.list-item') let item = $(element).parents('.list-item')
let id = item.attr('data-id') let id = item.attr('data-id')
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
resolve(store.flat(id)); resolve(store.flat(id, opt));
}); });
} }
@ -146,7 +150,7 @@ function editor(root, inputData, options) {
location.href = '/edit/' + id; location.href = '/edit/' + id;
}) })
} }
return true; return true;
}) })
cursor.set(0) cursor.set(0)
renderUpdate() renderUpdate()
@ -233,6 +237,10 @@ function editor(root, inputData, options) {
return {indented: indented, text: '', fold: 'open', hidden: false} return {indented: indented, text: '', fold: 'open', hidden: false}
} }
function hasMarkdown(text) {
return text.match(/[^a-zA-Z0-9 .,!@$&"'?]/)
}
function newItem(value) { function newItem(value) {
let el = document.createElement('div') let el = document.createElement('div')
el.classList.add('list-item') el.classList.add('list-item')
@ -249,7 +257,11 @@ function editor(root, inputData, options) {
line.prepend(content) line.prepend(content)
options.transform(value.text, $(content), value.id, EDITOR) if (hasMarkdown(value.text)) {
options.transform(value.text, $(content), value.id, EDITOR)
} else {
$(content).html(value.text)
}
let marker = document.createElement('span') let marker = document.createElement('span')
marker.classList.add('marker') marker.classList.add('marker')
@ -318,9 +330,12 @@ function editor(root, inputData, options) {
let hideLevel = 99999; let hideLevel = 99999;
let closedFolds = JSON.parse(localStorage.getItem('closed-folds') || '{}') || {}
$enter.each(function (index, li) { $enter.each(function (index, li) {
let storeId = enterData[index] let storeId = enterData[index]
let value = rootData.value(storeId) let value = rootData.value(storeId)
value.fold = closedFolds[value.id] ? 'closed' : 'open'
let hasChildren = false; let hasChildren = false;
if (index + 1 < last) { if (index + 1 < last) {
@ -336,7 +351,12 @@ function editor(root, inputData, options) {
value.hidden = value.indented >= hideLevel value.hidden = value.indented >= hideLevel
options.transform(value.text, $li.find('.content'), value.id, EDITOR) let $content = $li.find('.content');
if (hasMarkdown(value.text)) {
options.transform(value.text, $content, value.id, EDITOR)
} else {
$content.html(value.text)
}
if (value.indented < hideLevel) { if (value.indented < hideLevel) {
if (value.fold !== 'open') { if (value.fold !== 'open') {
@ -356,6 +376,8 @@ function editor(root, inputData, options) {
_.each(exitData, function (storeId, index) { _.each(exitData, function (storeId, index) {
let value = rootData.value(storeId) let value = rootData.value(storeId)
value.fold = closedFolds[value.id] ? 'closed' : 'open'
let $li = newItem(value) let $li = newItem(value)
.css('margin-left', (value.indented * 32) + 'px') .css('margin-left', (value.indented * 32) + 'px')
.toggleClass('selected', cursor.atPosition(index + $enter.length)) .toggleClass('selected', cursor.atPosition(index + $enter.length))
@ -396,15 +418,19 @@ function editor(root, inputData, options) {
} }
function enableDragging(rootElement) { function enableDragging(rootElement) {
let start = -1;
let startID = null;
let drake = dragula([rootElement], { let drake = dragula([rootElement], {
moves: function (el, container, handle, sibling) { moves: function (el, container, handle, sibling) {
return handle.classList.contains('marker') return handle.classList.contains('marker')
},
accepts: function (el, target, source, sibling) {
el.style.marginLeft = sibling === null ? 0 : sibling.style.marginLeft
return true
} }
}) })
let start = -1;
let startID = null;
drake.on('drag', function (el, source) { drake.on('drag', function (el, source) {
startID = $(el).attr('data-id') startID = $(el).attr('data-id')
}) })
@ -421,6 +447,7 @@ function editor(root, inputData, options) {
let newPosition = store.moveBefore(startID, stopID) let newPosition = store.moveBefore(startID, stopID)
cursor.set(newPosition[0]) cursor.set(newPosition[0])
// fix indent
_.defer(() => { _.defer(() => {
trigger('change') trigger('change')
@ -495,22 +522,86 @@ function editor(root, inputData, options) {
return true return true
} }
if (event.originalEvent.clipboardData.types.includes("text/html")) {
let pastedData = event.originalEvent.clipboardData.getData('text/html')
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
})
const markdown = turndownService.turndown(pastedData)
let items = markdown.split(/\n+/);
console.log(items)
let item = $(this).parents('.list-item')
let id = item.attr('data-id')
const firstItem = store.value(id)
items = _.map(items, text => {
const m = text.match(/^(\s*)\*\s*(.*)$/)
if (m) {
const item = newListItem(firstItem.indented+1+Math.trunc(m[1].length/4))
item.text = m[2]
return item
}
const item = newListItem(firstItem.indented)
item.text = text
return item
})
store.insertAfter(id, ...items)
trigger('change')
return false
}
let pastedData = event.originalEvent.clipboardData.getData('text/plain') let pastedData = event.originalEvent.clipboardData.getData('text/plain')
let items = JSON.parse(pastedData.toString()); try {
let items = JSON.parse(pastedData.toString());
let item = $(this).parents('.list-item')
let id = item.attr('data-id')
let item = $(this).parents('.list-item') // reset ids
let id = item.attr('data-id') _.each(items, item => {
item.id = null
})
// reset ids store.insertAfter(id, ...items)
_.each(items, item => { trigger('change')
item.id = null return false
}) } catch (e) {
let inputString = pastedData.toString()
if (inputString.match(/^- /)) {
let item = $(this).parents('.list-item')
let id = item.attr('data-id')
let lines = inputString.split(/^( *)- /ms)
lines.shift()
const firstItem = store.value(id)
const firstIndent = firstItem.indented
store.insertAfter(id, ...items) let items = _.map(_.chunk(lines, 2), line => {
const indent = Math.trunc(line[0].length / 4);
trigger('change') const item = newListItem(firstItem.indented+indent)
item.text = _.trimEnd(line[1]).replace(/\n/g, " \n")
return false return item
})
store.insertAfter(id, ...items)
trigger('change')
return false
} else {
let items = inputString.split(/\n+/);
let item = $(this).parents('.list-item')
let id = item.attr('data-id')
if (items.length === 1) {
return true
} else {
const firstItem = store.value(id)
items = _.map(items, text => {
const item = newListItem(firstItem.indented)
item.text = text
return item
})
store.insertAfter(id, ...items)
trigger('change')
}
return false
}
}
}); });
function moveCursor(event, dir) { function moveCursor(event, dir) {
@ -720,7 +811,7 @@ function editor(root, inputData, options) {
'{': '{}', '{': '{}',
} }
let c = prefix[prefix.length-1] let c = prefix[prefix.length - 1]
let braceSet = braces[c] let braceSet = braces[c]
let prefixCount = _(prefix) let prefixCount = _(prefix)

View File

@ -53,6 +53,11 @@
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.0.tgz", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.0.tgz",
"integrity": "sha1-LkYovhncSyFLXAJjDFlx6BFhgGI=" "integrity": "sha1-LkYovhncSyFLXAJjDFlx6BFhgGI="
}, },
"domino": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
"integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ=="
},
"dragula": { "dragula": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.2.tgz", "resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.2.tgz",
@ -164,6 +169,14 @@
"resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz", "resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz",
"integrity": "sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0=" "integrity": "sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0="
}, },
"turndown": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.1.1.tgz",
"integrity": "sha512-BEkXaWH7Wh7e9bd2QumhfAXk5g34+6QUmmWx+0q6ThaVOLuLUqsnkq35HQ5SBHSaxjSfSM7US5o4lhJNH7B9MA==",
"requires": {
"domino": "^2.1.6"
}
},
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@ -4,7 +4,9 @@
"description": "Simple editor of lists", "description": "Simple editor of lists",
"author": "Peter Stuifzand <peter@p83.nl>", "author": "Peter Stuifzand <peter@p83.nl>",
"main": "index.js", "main": "index.js",
"sideEffects": ["**"], "sideEffects": [
"**"
],
"scripts": { "scripts": {
"test": "jasmine --require=esm" "test": "jasmine --require=esm"
}, },
@ -14,7 +16,8 @@
"dragula": "^3.7.2", "dragula": "^3.7.2",
"he": "^1.2.0", "he": "^1.2.0",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"lodash": "^4.17.19" "lodash": "^4.17.19",
"turndown": "^7.1.1"
}, },
"devDependencies": { "devDependencies": {
"esm": "^3.2.25", "esm": "^3.2.25",

View File

@ -47,4 +47,58 @@ describe("A cursor", function() {
expect(this.cursor.get()).toBe(3) expect(this.cursor.get()).toBe(3)
}) })
}) })
describe("with a store with current closed", function () {
beforeEach(function () {
this.store = createStore([
{indented:0, fold: 'open'},
{indented:1, fold: 'open'},
{indented:2, fold: 'open'},
{indented:3, fold: 'open'},
{indented:1, fold: 'closed', hidden: true},
])
})
it("moveUp moves up by one", function() {
this.cursor.set(4)
this.cursor.moveUp(this.store)
expect(this.cursor.get()).toBe(3)
})
})
describe("with a store with above closed", function () {
beforeEach(function () {
this.store = createStore([
{indented:0, fold: 'open'},
{indented:1, fold: 'open'},
{indented:2, fold: 'open'},
{indented:3, fold: 'closed', hidden: true},
{indented:1, fold: 'open'},
])
})
it("moveUp moves up by one", function() {
this.cursor.set(4)
this.cursor.moveUp(this.store)
expect(this.cursor.get()).toBe(2)
})
})
describe("with a store with multiple above closed", function () {
beforeEach(function () {
this.store = createStore([
{indented:0, fold: 'open'},
{indented:1, fold: 'open'},
{indented:2, fold: 'closed', hidden: true},
{indented:3, fold: 'closed', hidden: true},
{indented:1, fold: 'open'},
])
})
it("moveUp moves up by one", function() {
this.cursor.set(4)
this.cursor.moveUp(this.store)
expect(this.cursor.get()).toBe(1)
})
})
}) })

View File

@ -189,4 +189,40 @@ describe("A store", function () {
expect(store.debug().idList).toEqual(["_a", "_c", "_b", "_d"]) expect(store.debug().idList).toEqual(["_a", "_c", "_b", "_d"])
}) })
}) })
describe("contains a moveBefore method which handles indentation", function () {
let store
beforeEach(function () {
store = createStore([
{text: "a", id: "_a", indented: 0},
{text: "b", id: "_b", indented: 1},
{text: "c", id: "_c", indented: 2},
{text: "d", id: "_d", indented: 3},
{text: "e", id: "_e", indented: 0},
])
})
it("moveBefore _e, _c", function () {
store.moveBefore("_e", "_c")
let debug = store.debug();
expect(debug.idList).toEqual(["_a", "_b", "_e", "_c", "_d"])
expect(debug.result[2]).toEqual({text: "e", id: "_e", indented: 2})
})
it("moveBefore _d, at-end", function () {
store.moveBefore("_d", "at-end")
let debug = store.debug();
expect(debug.idList).toEqual(["_a", "_b", "_c", "_e", "_d"])
expect(debug.result[4]).toEqual({text: "d", id: "_d", indented: 0})
})
it("moveBefore _b, at-end", function () {
store.moveBefore("_b", "at-end")
let debug = store.debug();
expect(debug.idList).toEqual(["_a", "_e", "_b", "_c", "_d"])
expect(debug.result[0]).toEqual({text: "a", id: "_a", indented: 0})
expect(debug.result[1]).toEqual({text: "e", id: "_e", indented: 0})
expect(debug.result[2]).toEqual({text: "b", id: "_b", indented: 0})
expect(debug.result[3]).toEqual({text: "c", id: "_c", indented: 1})
expect(debug.result[4]).toEqual({text: "d", id: "_d", indented: 2})
})
})
}) })

View File

@ -55,21 +55,15 @@ function Store(inputData) {
} }
function prevCursorPosition(cursor) { function prevCursorPosition(cursor) {
let curIndent = values[idList[cursor]].indented
let curClosed = values[idList[cursor]].fold !== 'open';
if (!curClosed) {
curIndent = 10000000;
}
let moving = true let moving = true
while (moving) { while (moving) {
cursor-- cursor--
if (cursor < 0) { if (cursor < 0) {
cursor = idList.length - 1 cursor = idList.length - 1
curIndent = values[idList[cursor]].indented
} }
let next = values[idList[cursor]]; let next = values[idList[cursor]];
if (curIndent >= next.indented && !next.hidden) { if (!next.hidden) {
moving = false moving = false
} }
} }
@ -203,9 +197,20 @@ function Store(inputData) {
values[currentId] = newValue values[currentId] = newValue
changed = true changed = true
} }
updateFold(currentId, newValue.fold === 'open')
return currentId return currentId
} }
function updateFold(id, open) {
let closedFolds = JSON.parse(localStorage.getItem("closed-folds") || '{}') || {}
if (open) {
delete closedFolds[id]
} else {
closedFolds[id] = true
}
localStorage.setItem('closed-folds', JSON.stringify(closedFolds))
}
function length() { function length() {
return idList.length; return idList.length;
} }
@ -301,7 +306,19 @@ function Store(inputData) {
rotate(idList, toIndex, fromIndex + n, fromIndex - toIndex) rotate(idList, toIndex, fromIndex + n, fromIndex - toIndex)
changed = true changed = true
} }
return [index(from), n]
// Copy indent from the next item, or 0 when at the end
const v = value(to)
const diff = (v ? v.indented : 0) - value(from).indented
let first = index(from)
let i = 0
while (i < n) {
value(idList[first + i]).indented += diff
i++
}
return [first, n]
} }
/** /**
@ -342,8 +359,23 @@ function Store(inputData) {
return [values[from], ..._.takeWhile(items, item => item.indented > values[from].indented)] return [values[from], ..._.takeWhile(items, item => item.indented > values[from].indented)]
} }
function flat(from) { function flat(from, opt) {
return selectItemsFrom(from) opt = opt || {}
let result = selectItemsFrom(from)
if (opt.base && result.length > 0) {
const first = result[0]
let children = _.map(result.slice(1), (item) => {
let newItem = _.clone(item)
newItem.indented -= first.indented+1
newItem.id = ID()
return newItem
})
return {
title: first.text,
children
}
}
return result
} }
/** /**
@ -455,7 +487,7 @@ function Store(inputData) {
let removeLevel = 9999999999; let removeLevel = 9999999999;
_.each(inputData, (d) => { _.each(inputData, (d) => {
if (d.text.startsWith("{{query:")) { if (d.text && d.text.startsWith("{{query:")) {
removeLevel = d.indented; removeLevel = d.indented;
append(d) append(d)
} else if (d.indented <= removeLevel) { } else if (d.indented <= removeLevel) {

191
main.go
View File

@ -28,6 +28,7 @@ import (
"html/template" "html/template"
"io" "io"
"log" "log"
"math/rand"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -36,14 +37,16 @@ import (
"strings" "strings"
"time" "time"
"github.com/blevesearch/bleve"
"p83.nl/go/ekster/pkg/util" "p83.nl/go/ekster/pkg/util"
"p83.nl/go/indieauth" "p83.nl/go/indieauth"
"p83.nl/go/wiki/link" "p83.nl/go/wiki/link"
"github.com/blevesearch/bleve/v2"
) )
func init() { func init() {
log.SetFlags(log.Lshortfile) log.SetFlags(log.Lshortfile)
rand.Seed(time.Now().UnixNano())
} }
type authorizedKey string type authorizedKey string
@ -52,14 +55,8 @@ var (
authKey = authorizedKey("authorizedKey") authKey = authorizedKey("authorizedKey")
) )
type BlockRepository interface {
GetBlock(id string) (Block, error)
GetBlocks(rootBlockID string) (BlockResponse, error)
SaveBlock(id string, block Block) error
}
var ( var (
mp PagesRepository mp PagesRepository
port = flag.Int("port", 8080, "listen port") port = flag.Int("port", 8080, "listen port")
baseurl = flag.String("baseurl", "", "baseurl") baseurl = flag.String("baseurl", "", "baseurl")
@ -235,7 +232,7 @@ func (*authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
if r.URL.Path == "/auth/login" { if r.URL.Path == "/auth/login" {
templates := baseTemplate templates := []string{"templates/layout_no_sidebar.html"}
templates = append(templates, "templates/login.html") templates = append(templates, "templates/login.html")
t, err := template.ParseFiles(templates...) t, err := template.ParseFiles(templates...)
if err != nil { if err != nil {
@ -295,8 +292,12 @@ func (*authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(urlString) u, err := url.Parse(urlString)
if err != nil { if err != nil {
http.Error(w, err.Error(), 400) urlString = fmt.Sprintf("https://%s/", urlString)
return u, err = url.Parse(urlString)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
} }
endpoints, err := indieauth.GetEndpoints(u) endpoints, err := indieauth.GetEndpoints(u)
@ -744,14 +745,13 @@ func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
metaKV, err := regexp.Compile(`(\w+)::\s+(.*)`) metaKV, err := regexp.Compile(`(\w[ \w]*)::(?: +(.*))?`)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
sess, err := NewSession(w, r) sess, err := NewSession(w, r)
if err != nil { if err != nil {
log.Println("NewSession", err)
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
@ -812,7 +812,6 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if format == "metakv" { } else if format == "metakv" {
so, err := createStructuredFormat(mpPage) so, err := createStructuredFormat(mpPage)
if err != nil { if err != nil {
log.Println("createStructuredFormat", err)
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
@ -822,7 +821,6 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
enc.SetIndent("", " ") enc.SetIndent("", " ")
err = enc.Encode(so) err = enc.Encode(so)
if err != nil { if err != nil {
log.Println("Encode", err)
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
} }
return return
@ -848,7 +846,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, line := range lines { for _, line := range lines {
if first { if first {
builder.WriteString(strings.Repeat(" ", item.Indented)) builder.WriteString(strings.Repeat(" ", item.Indented))
builder.WriteString("* ") builder.WriteString("- ")
builder.WriteString(line) builder.WriteString(line)
builder.WriteByte('\n') builder.WriteByte('\n')
first = false first = false
@ -865,7 +863,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
builder.WriteString(strings.Repeat(" ", item.Indented)) builder.WriteString(strings.Repeat(" ", item.Indented))
builder.WriteString("* ") builder.WriteString("- ")
builder.WriteString(item.Text) builder.WriteString(item.Text)
builder.WriteByte('\n') builder.WriteByte('\n')
} }
@ -881,7 +879,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
if format == "html" { if format == "html" {
pageText = metaKV.ReplaceAllString(pageText, "**[[$1]]**: $2") pageText = metaKV.ReplaceAllString(pageText, "**$1**: $2")
pageText = renderLinks(pageText, false) pageText = renderLinks(pageText, false)
pageText = renderMarkdown2(pageText) pageText = renderMarkdown2(pageText)
@ -901,7 +899,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ShowGraph: page != "Daily_Notes", ShowGraph: page != "Daily_Notes",
TodayPage: "Today", TodayPage: "Today",
} }
templates := baseTemplate templates := []string{"templates/layout_no_sidebar.html"}
templates = append(templates, "templates/view.html") templates = append(templates, "templates/view.html")
t, err := template.ParseFiles(templates...) t, err := template.ParseFiles(templates...)
if err != nil { if err != nil {
@ -927,11 +925,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
func renderLinks(pageText string, edit bool) string { func renderLinks(pageText string, edit bool) string {
hrefRE, err := regexp.Compile(`#?\[\[\s*([^\]]+)\s*\]\]`) hrefRE := regexp.MustCompile(`#?\[\[\s*([^\]]+)\s*\]\]`)
if err != nil {
log.Fatal(err)
}
pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string { pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string {
tag := false tag := false
if s[0] == '#' { if s[0] == '#' {
@ -943,8 +937,16 @@ func renderLinks(pageText string, edit bool) string {
s = strings.TrimSuffix(s, "]]") s = strings.TrimSuffix(s, "]]")
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
if tag { if tag {
return fmt.Sprintf(`<a href=%q class="tag">%s</a>`, cleanNameURL(s), s) switch s {
case "TODO":
return fmt.Sprint(`[ ] `)
case "DONE":
return fmt.Sprint(`[X] `)
default:
return fmt.Sprintf(`<a href=%q class="tag">%s</a>`, cleanNameURL(s), s)
}
} }
editPart := "" editPart := ""
if edit { if edit {
editPart = "edit/" editPart = "edit/"
@ -952,6 +954,14 @@ func renderLinks(pageText string, edit bool) string {
return fmt.Sprintf("[%s](/%s%s)", s, editPart, cleanNameURL(s)) return fmt.Sprintf("[%s](/%s%s)", s, editPart, cleanNameURL(s))
}) })
tagRE := regexp.MustCompile(`#(\S+)`)
pageText = tagRE.ReplaceAllStringFunc(pageText, func(s string) string {
s = strings.TrimPrefix(s, "#")
s = strings.TrimSpace(s)
return fmt.Sprintf(`<a href="/%s" class="tag">%s</a>`, url.PathEscape(cleanNameURL(s)), s)
})
return pageText return pageText
} }
@ -1053,8 +1063,39 @@ func main() {
} }
mp = NewFilePages(dataDir, searchIndex) mp = NewFilePages(dataDir, searchIndex)
repo := NewBlockRepo("data")
http.Handle("/auth/", &authHandler{}) http.Handle("/auth/", &authHandler{})
http.HandleFunc("/api/block/view", wrapAuth(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if !r.Context().Value(authKey).(bool) {
http.Error(w, "Unauthorized", 401)
return
}
if r.Method != "GET" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
id := r.URL.Query().Get("id")
block, err := repo.Load(id)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
err = enc.Encode(block)
if err != nil {
http.Error(w, err.Error(), 500)
}
}))
http.HandleFunc("/api/block/", wrapAuth(func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/api/block/", wrapAuth(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
@ -1112,7 +1153,36 @@ func main() {
} }
} }
})) }))
http.HandleFunc("/api/block/update", wrapAuth(func(w http.ResponseWriter, r *http.Request) {})) http.HandleFunc("/api/block/replace", wrapAuth(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), 400)
return
}
id := r.Form.Get("id")
if id == "" {
http.Error(w, "missing id", 400)
return
}
block, err := repo.Load(id)
block.Text = r.Form.Get("text")
err = repo.Save(id, block)
// update search index
searchObjects, err := createSearchObjects(id)
batch := searchIndex.NewBatch()
for _, so := range searchObjects {
err = batch.Index(so.ID, so)
if err != nil {
log.Println(err)
}
}
err = searchIndex.Batch(batch)
if err != nil {
log.Println(err)
}
// TODO: update backlinks
return
}))
http.HandleFunc("/api/block/append", wrapAuth(func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/api/block/append", wrapAuth(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
@ -1124,46 +1194,38 @@ func main() {
http.Error(w, "missing id", 400) http.Error(w, "missing id", 400)
return return
} }
newBlock := Block{
page := mp.Get(id) Text: r.Form.Get("text"),
log.Println(page.Content) Children: []string{},
var listItems []ListItem Parent: id,
id = page.Name // Use the name that was actually loaded
err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
if err != nil && err != io.EOF {
http.Error(w, fmt.Sprintf("while decoding: %s", err.Error()), 500)
return
} }
newId := &ID{"1", true} newId := &ID{"1", true}
generatedID := newId.NewID() generatedID := newId.NewID()
listItems = append(listItems, ListItem{ err = repo.Save(generatedID, newBlock)
ID: generatedID, block, err := repo.Load(id)
Indented: 0, block.Children = append(block.Children, generatedID)
Text: r.Form.Get("text"), err = repo.Save(id, block)
Fleeting: false,
})
var buf bytes.Buffer // update search index
sw := stopwatch{}
sw.Start("createSearchObjects")
searchObjects, err := createSearchObjects(id)
err = json.NewEncoder(&buf).Encode(&listItems) batch := searchIndex.NewBatch()
if err != nil {
http.Error(w, fmt.Sprintf("while encoding: %s", err.Error()), 500) for _, so := range searchObjects {
return err = batch.Index(so.ID, so)
if err != nil {
log.Println(err)
}
} }
page.Content = buf.String() err = searchIndex.Batch(batch)
page.Name = id
page.Title = id
err = mp.Save(id, page, "", "")
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("while saving: %s", err.Error()), 500) log.Println(err)
return
} }
fmt.Println(generatedID) sw.Stop()
return return
})) }))
http.HandleFunc("/links.json", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/links.json", func(w http.ResponseWriter, r *http.Request) {
@ -1268,9 +1330,9 @@ func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
indexMapping := bleve.NewIndexMapping() indexMapping := bleve.NewIndexMapping()
documentMapping := bleve.NewDocumentMapping() documentMapping := bleve.NewDocumentMapping()
nameFieldMapping := bleve.NewTextFieldMapping() pageFieldMapping := bleve.NewTextFieldMapping()
nameFieldMapping.Store = true pageFieldMapping.Store = true
documentMapping.AddFieldMappingsAt("name", nameFieldMapping) documentMapping.AddFieldMappingsAt("page", pageFieldMapping)
titleFieldMapping := bleve.NewTextFieldMapping() titleFieldMapping := bleve.NewTextFieldMapping()
titleFieldMapping.Store = true titleFieldMapping.Store = true
@ -1280,6 +1342,15 @@ func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
linkFieldMapping.Store = true linkFieldMapping.Store = true
documentMapping.AddFieldMappingsAt("link", linkFieldMapping) documentMapping.AddFieldMappingsAt("link", linkFieldMapping)
textFieldMapping := bleve.NewTextFieldMapping()
textFieldMapping.Store = true
documentMapping.AddFieldMappingsAt("text", textFieldMapping)
dateFieldMapping := bleve.NewDateTimeFieldMapping()
dateFieldMapping.Store = false
dateFieldMapping.Index = true
documentMapping.AddFieldMappingsAt("date", dateFieldMapping)
indexMapping.AddDocumentMapping("block", documentMapping) indexMapping.AddDocumentMapping("block", documentMapping)
searchIndex, err := bleve.New(indexDir, indexMapping) searchIndex, err := bleve.New(indexDir, indexMapping)
@ -1294,7 +1365,7 @@ func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
} }
for _, page := range pages { for _, page := range pages {
searchObjects, err := createSearchObjects(fp, page.Name) searchObjects, err := createSearchObjects(page.Name)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
continue continue

135
search.go
View File

@ -25,9 +25,10 @@ import (
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time"
"github.com/blevesearch/bleve" "github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/mapping" "github.com/blevesearch/bleve/v2/mapping"
"github.com/iancoleman/strcase" "github.com/iancoleman/strcase"
) )
@ -53,6 +54,7 @@ type searchObject struct {
Refs []nameLine `json:"refs"` Refs []nameLine `json:"refs"`
Meta map[string]interface{} `json:"meta"` Meta map[string]interface{} `json:"meta"`
Links []ParsedLink `json:"links"` Links []ParsedLink `json:"links"`
Dates []time.Time `json:"dates"`
} }
func NewSearchHandler(searchIndex bleve.Index) (http.Handler, error) { func NewSearchHandler(searchIndex bleve.Index) (http.Handler, error) {
@ -104,6 +106,9 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
if r.PostForm.Get("reset") == "1" { if r.PostForm.Get("reset") == "1" {
var sw stopwatch
sw.Start("full reset")
defer sw.Stop()
refs := make(Refs) refs := make(Refs)
mp := NewFilePages("data", nil) mp := NewFilePages("data", nil)
@ -115,13 +120,15 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
for _, page := range pages { for _, page := range pages {
err = mp.saveBlocksFromPage("data", page) err = saveBlocksFromPage("data", page)
if err != nil { if err != nil {
log.Printf("error while processing blocks from page %s: %v", page.Name, err) log.Printf("error while processing blocks from page %s: %v", page.Name, err)
continue continue
} }
} }
sw.Lap("save blocks from pages")
// Reload all pages // Reload all pages
pages, err = mp.AllPages() pages, err = mp.AllPages()
if err != nil { if err != nil {
@ -138,21 +145,23 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
log.Println("saveLinks") sw.Lap("process backrefs for pages")
err = saveLinks(mp) err = saveLinks(mp)
if err != nil { if err != nil {
log.Printf("error while saving links %v", err) log.Printf("error while saving links %v", err)
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
sw.Lap("save links")
log.Println("saveBackrefs")
err = saveBackrefs("data/backrefs.json", refs) err = saveBackrefs("data/backrefs.json", refs)
if err != nil { if err != nil {
log.Printf("error while saving backrefs %v", err) log.Printf("error while saving backrefs %v", err)
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
sw.Lap("save backrefs")
err = os.RemoveAll("data/_tmp_index") err = os.RemoveAll("data/_tmp_index")
if err != nil { if err != nil {
@ -161,28 +170,12 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
index, err := createSearchIndex("data", "_tmp_index") _, err = createSearchIndex("data", "_tmp_index")
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
for _, page := range pages {
searchObjects, err := createSearchObjects(mp, page.Name)
if err != nil {
log.Printf("error while creating search object %s: %v", page.Title, err)
continue
}
for _, so := range searchObjects {
err = index.Index(so.ID, so)
if err != nil {
log.Printf("error while indexing %s: %v", page.Title, err)
continue
}
}
}
err = os.Rename("data/_page-index", "data/_page-index-old") err = os.Rename("data/_page-index", "data/_page-index-old")
if err != nil { if err != nil {
log.Printf("error while resetting index: %v", err) log.Printf("error while resetting index: %v", err)
@ -201,6 +194,7 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
sw.Lap("indexing")
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
enc.SetIndent("", " ") enc.SetIndent("", " ")
@ -219,7 +213,7 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sr := bleve.NewSearchRequest(q) sr := bleve.NewSearchRequest(q)
sr.IncludeLocations = false sr.IncludeLocations = false
sr.Size = 25 sr.Size = 25
sr.Fields = []string{"page", "title", "text"} sr.Fields = []string{"page", "title", "text", "date", "parent"}
sr.Highlight = bleve.NewHighlightWithStyle("html") sr.Highlight = bleve.NewHighlightWithStyle("html")
sr.Highlight.AddField("text") sr.Highlight.AddField("text")
results, err := s.searchIndex.Search(sr) results, err := s.searchIndex.Search(sr)
@ -235,23 +229,36 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
type pageBlock struct { type pageBlock struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Parent string `json:"parent"`
Page string `json:"page"` Title string `json:"title"`
Text string `json:"text"` Page string `json:"page"`
Link string `json:"link"` Text string `json:"text"`
Link []string `json:"link"`
Tag []string `json:"tag"`
Date []time.Time `json:"date"`
Key string `json:"key"`
Value string `json:"value"`
} }
func (p pageBlock) Type() string { func (p pageBlock) Type() string {
return "block" return "block"
} }
func createSearchObjects(fp *FilePages, rootBlockID string) ([]pageBlock, error) { func createSearchObjects(rootBlockID string) ([]pageBlock, error) {
blocks, err := fp.blockRepo.GetBlocks(rootBlockID) log.Println("createSearchObjects", rootBlockID)
blocks, err := loadBlocks("data", rootBlockID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(blocks.Parents) > 0 {
page := blocks.Parents[len(blocks.Parents)-1]
if page != rootBlockID {
blocks, err = loadBlocks("data", page)
}
}
var pageBlocks []pageBlock var pageBlocks []pageBlock
queue := []string{blocks.PageID} queue := []string{blocks.PageID}
@ -262,29 +269,49 @@ func createSearchObjects(fp *FilePages, rootBlockID string) ([]pageBlock, error)
links, err := ParseLinks(current, blocks.Texts[current]) links, err := ParseLinks(current, blocks.Texts[current])
if err != nil { if err != nil {
continue log.Println("ParseLinks", err)
links = nil
} }
if len(links) == 0 { var linkNames []string
pageBlocks = append(pageBlocks, pageBlock{ for _, link := range links {
ID: current, linkNames = append(linkNames, link.Name)
Title: blocks.Texts[blocks.PageID],
Page: blocks.PageID,
Text: blocks.Texts[current],
Link: "",
})
} else {
for _, link := range links {
pageBlocks = append(pageBlocks, pageBlock{
ID: current,
Title: blocks.Texts[blocks.PageID],
Page: blocks.PageID,
Text: blocks.Texts[current],
Link: link.Name,
})
}
} }
tags, err := ParseTags(blocks.Texts[current])
if err != nil {
log.Println("ParseTags", err)
tags = nil
}
dates, err := ParseDates(blocks.Texts[current])
if err != nil {
log.Println("ParseDates", err)
dates = nil
}
pageDate, err := ParseDatePageName(blocks.Texts[blocks.PageID])
if err == nil {
dates = append(dates, pageDate)
}
block := pageBlock{
ID: current,
Parent: blocks.ParentID,
Title: blocks.Texts[blocks.PageID],
Page: blocks.PageID,
Text: blocks.Texts[current],
Link: linkNames,
Tag: tags,
Date: dates,
}
if kvpair := strings.SplitN(blocks.Texts[current], "::", 2); len(kvpair) == 2 {
block.Key = strings.TrimSpace(kvpair[0])
block.Value = strings.TrimSpace(kvpair[1])
}
pageBlocks = append(pageBlocks, block)
queue = append(queue, blocks.Children[current]...) queue = append(queue, blocks.Children[current]...)
} }
@ -400,9 +427,19 @@ func createStructuredFormat(page Page) (searchObject, error) {
} }
so.Links = append(so.Links, links...) so.Links = append(so.Links, links...)
dates, err := ParseDates(li.Text)
if err != nil {
dates = nil
}
so.Dates = append(so.Dates, dates...)
} }
} }
date, err := ParseDatePageName(so.Title)
if err == nil {
so.Dates = append(so.Dates, date)
}
// merge up // merge up
for len(parents) > 1 { for len(parents) > 1 {
par := parents[len(parents)-1] par := parents[len(parents)-1]

View File

@ -24,7 +24,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"sync"
"time" "time"
) )
@ -41,12 +40,12 @@ type Session struct {
func NewSession(w http.ResponseWriter, r *http.Request) (*Session, error) { func NewSession(w http.ResponseWriter, r *http.Request) (*Session, error) {
sessionID, err := getSessionCookie(w, r) sessionID, err := getSessionCookie(w, r)
if err != nil { if err != nil {
return nil, fmt.Errorf("getSessionCookie failed: %w", err) return nil, err
} }
session := &Session{ID: sessionID} session := &Session{ID: sessionID}
err = loadSession(session) err = loadSession(session)
if err != nil { if err != nil {
return nil, fmt.Errorf("loadSession failed: %w" , err) return nil, err
} }
return session, nil return session, nil
} }
@ -55,13 +54,9 @@ func (sess *Session) Flush() error {
return saveSession(sess) return saveSession(sess)
} }
var fileMutex sync.RWMutex
func saveSession(sess *Session) error { func saveSession(sess *Session) error {
filename := generateFilename(sess.ID) filename := generateFilename(sess.ID)
err := os.Mkdir("session", 0755) err := os.Mkdir("session", 0755)
fileMutex.Lock()
defer fileMutex.Unlock()
f, err := os.Create(filename) f, err := os.Create(filename)
if err != nil { if err != nil {
return err return err
@ -74,22 +69,17 @@ func saveSession(sess *Session) error {
func loadSession(sess *Session) error { func loadSession(sess *Session) error {
filename := generateFilename(sess.ID) filename := generateFilename(sess.ID)
err := os.Mkdir("session", 0755) err := os.Mkdir("session", 0755)
fileMutex.RLock()
defer fileMutex.RUnlock()
f, err := os.Open(filename) f, err := os.Open(filename)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
// add defaults to session? // add defaults to session?
return nil return nil
} }
return fmt.Errorf("while opening file %s: %w", filename, err) return err
} }
defer f.Close() defer f.Close()
err = json.NewDecoder(f).Decode(sess) err = json.NewDecoder(f).Decode(sess)
if err != nil { return err
return fmt.Errorf("while decoding json from file %s: %w", filename, err)
}
return nil
} }
func generateFilename(id string) string { func generateFilename(id string) string {

View File

@ -18,23 +18,6 @@
<input type="hidden" name="p" value="{{ .Name }}"/> <input type="hidden" name="p" value="{{ .Name }}"/>
{{ .Editor }} {{ .Editor }}
</form> </form>
{{ if .Backrefs }}
<div class="backrefs content">
<h3>Linked references</h3>
<ul>
{{ range $name, $refs := .Backrefs }}
<li><a href="/edit/{{ $name }}">{{ (index $refs 0).Title }}</a>
<ul>
{{ range $ref := $refs }}
<li>{{ $ref.LineEditHTML }}</li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
</div>
{{ end }}
</div> </div>
</div> </div>
{{ end }} {{ end }}

View File

@ -0,0 +1,83 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="redirect_uri" href="{{ .RedirectURI }}" />
<link rel="stylesheet" href="/public/main.css" />
<link rel="stylesheet" href="/public/styles.css" />
<!-- <link rel="stylesheet" href="https://unpkg.com/prismjs@1.20.0/themes/prism-tomorrow.css"> -->
<!-- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/> -->
<title>{{ .Title }} - Wiki</title>
{{ block "content_head" . }} {{ end }}
<style>
</style>
<style>
</style>
</head>
<body data-base-url="{{ .BaseURL }}">
<div>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
Wiki
</a>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
{{ block "navbar" . }}{{ end }}
</div>
</div>
</nav>
<div class="searchbar">
<div class="field">
<p class="control">
<input class="search input" id="search-input" type="text" placeholder="Find a page">
</p>
<div id="autocomplete" class="hide keyboard-list" tabindex="0"></div>
</div>
</div>
<div style="padding:0 32px;">
<section class="section">
{{ template "content" . }}
</section>
</div>
<div class="footer">
<div class="h-app">
<a href="/" class="u-url p-name">Wiki</a>
&mdash; created by <a href="https://peterstuifzand.nl/">Peter Stuifzand</a>
</div>
</div>
</div>
<div id="link-complete" class="hide keyboard-list"></div>
{{ block "footer_scripts" . }}
{{ end }}
<div id="result-template" class="hide">
<ul>
[[#results]]
<li><a href="/edit/[[ref]]">[[title]] <div>[[&amp; text]]</div></a></li>
[[/results]]
[[^results]]
<li>No results</li>
[[/results]]
<li><a href="/edit/[[page]]">Create a page</a></li>
</ul>
</div>
<script async src="/public/vendors.js"></script>
<script async src="/public/main.js"></script>
</body>
</html>

View File

@ -1,12 +1,48 @@
{{ define "sidebar-right" }} {{ define "sidebar-right" }}
<div class="sidebar-right"> <div class="sidebar-right">
{{block "calendar" .}} <div class="tab-bar">
{{end}} <div class="tab tab-active" data-target="#tab-calendar">
<span>Calendar</span>
</div>
<div class="tab" data-target="#tab-graph">
<span>Graph</span>
</div>
<div class="tab" data-target="#tab-linked-refs">
<span>Backlinks</span>
</div>
</div>
<div class="tabs">
<div class="tab-page" id="tab-calendar">
{{block "calendar" .}}
{{end}}
</div>
<div class="graph-view"> <div class="tab-page" id="tab-graph">
{{ if .ShowGraph }} <div class="graph-view">
<div class="graph-network" data-name="{{ .Name }}" style="height:80vh; top:0; position: sticky"></div> {{ if .ShowGraph }}
{{ end }} <div class="graph-network" data-name="{{ .Name }}" style="height:80vh; top:0; position: sticky"></div>
{{ end }}
</div>
</div>
<div class="tab-page" id="tab-linked-refs">
{{ if .Backrefs }}
<div class="backrefs content">
<h3>Linked references</h3>
<ul>
{{ range $name, $refs := .Backrefs }}
<li><a href="/edit/{{ $name }}">{{ (index $refs 0).Title }}</a>
<ul>
{{ range $ref := $refs }}
<li>{{ $ref.LineEditHTML }}</li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
</div>
{{ end }}
</div>
</div> </div>
</div> </div>
{{ end }} {{ end }}

View File

@ -22,11 +22,30 @@
</ul> </ul>
</div> </div>
</div> </div>
</div>
<div class="column"> <div class="modal review-modal">
{{ if .ShowGraph }} <div class="modal-background"></div>
<div class="graph-network" data-name="{{ .Name }}" style="height:80vh; top:0; position: sticky"></div> <div class="modal-card">
{{ end }} <header class="modal-card-head">
<p class="modal-card-title">Review</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body" style="min-height: 400px">
<h3 class="block-title is-title"></h3>
<textarea class="textarea block-text" style="height:380px"></textarea>
<div class="end-of-review hide">All tasks are reviewed.</p>
</section>
<div class="modal-card-foot" style="justify-content: center">
<button class="button normal is-danger review" data-review="again">Again</button>
<button class="button hard is-warning review" data-review="soon">Soon</button>
<button class="button easy is-success review" data-review="later">Later</button>
<button class="button easy is-default review" data-review="never">Never</button>
<button class="button end-of-review hide close">Close</button>
</div>
</div> </div>
</div> </div>
{{ end }} {{ end }}
@ -38,6 +57,8 @@
<a href="/recent/" class="navbar-item">Recent Changes</a> <a href="/recent/" class="navbar-item">Recent Changes</a>
<a href="/graph/" class="navbar-item">Graph</a> <a href="/graph/" class="navbar-item">Graph</a>
<a href="/{{ .TodayPage }}" class="navbar-item">Today</a> <a href="/{{ .TodayPage }}" class="navbar-item">Today</a>
<a href="" class="navbar-item start-review">Review</a>
<a href="" class="navbar-item start-sr">SR</a>
<a href="/auth/logout" class="navbar-item">Logout</a> <a href="/auth/logout" class="navbar-item">Logout</a>
<span class="navbar-item"><b>{{ $.Session.Me }}</b></span> <span class="navbar-item"><b>{{ $.Session.Me }}</b></span>
{{ else }} {{ else }}
@ -50,9 +71,5 @@
.edit { .edit {
color: red; color: red;
} }
.tag {
color: #444;
}
</style> </style>
{{ end }} {{ end }}

65
util.go
View File

@ -29,6 +29,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"p83.nl/go/wiki/link"
) )
var ( var (
@ -70,6 +72,25 @@ func RandStringBytes(n int) string {
return string(b) return string(b)
} }
type DateLink struct {
Link string
Date time.Time
}
func ParseDates(content string) ([]time.Time, error) {
links := link.FindAllLinks(content)
var result []time.Time
for _, linkName := range links {
date, err := ParseDatePageName(linkName)
if err != nil {
continue
}
result = append(result, date)
}
return result, nil
}
func ParseLinks(blockId string, content string) ([]ParsedLink, error) { func ParseLinks(blockId string, content string) ([]ParsedLink, error) {
hrefRE := regexp.MustCompile(`(#?\[\[\s*([^\]]+)\s*\]\])`) hrefRE := regexp.MustCompile(`(#?\[\[\s*([^\]]+)\s*\]\])`)
// keywordsRE := regexp.MustCompile(`(\w+)::`) // keywordsRE := regexp.MustCompile(`(\w+)::`)
@ -110,6 +131,33 @@ func ParseLinks(blockId string, content string) ([]ParsedLink, error) {
return result, nil return result, nil
} }
func ParseTags(content string) ([]string, error) {
linkRE := regexp.MustCompile(`(#\[\[\s*([^\]]+)\s*\]\])`)
tagRE := regexp.MustCompile(`#([^ ]+)`)
scanner := bufio.NewScanner(strings.NewReader(content))
scanner.Split(bufio.ScanLines)
var result []string
for scanner.Scan() {
line := scanner.Text()
links := linkRE.FindAllStringSubmatch(line, -1)
for _, matches := range links {
linkText := matches[0]
linkText = strings.TrimPrefix(linkText, "#[[")
linkText = strings.TrimSuffix(linkText, "]]")
linkText = strings.TrimSpace(linkText)
result = append(result, linkText)
}
for _, matches := range tagRE.FindAllStringSubmatch(line, -1) {
result = append(result, matches[1])
}
}
return result, nil
}
func cleanNameURL(name string) string { func cleanNameURL(name string) string {
return strings.Replace(name, " ", "_", -1) return strings.Replace(name, " ", "_", -1)
} }
@ -119,15 +167,24 @@ func cleanTitle(name string) string {
} }
type stopwatch struct { type stopwatch struct {
start time.Time start time.Time
label string lastLap time.Time
label string
} }
func (sw *stopwatch) Start(label string) { func (sw *stopwatch) Start(label string) {
sw.start = time.Now() sw.start = time.Now()
sw.lastLap = time.Now()
sw.label = label sw.label = label
} }
func (sw *stopwatch) Lap(label string) {
now := time.Now()
d := now.Sub(sw.lastLap)
log.Printf("%-20s: %s\n", label, d.String())
sw.lastLap = now
}
func (sw *stopwatch) Stop() { func (sw *stopwatch) Stop() {
endTime := time.Now() endTime := time.Now()
d := endTime.Sub(sw.start) d := endTime.Sub(sw.start)
@ -176,7 +233,7 @@ func parseMonth(month string) (time.Month, error) {
} }
func ParseDatePageName(name string) (time.Time, error) { func ParseDatePageName(name string) (time.Time, error) {
if matches := niceDateParseRE.FindStringSubmatch(name); matches != nil { if matches := niceDateParseRE.FindStringSubmatch(strings.Replace(name, " ", "_", -1)); matches != nil {
day, err := strconv.Atoi(matches[1]) day, err := strconv.Atoi(matches[1])
if err != nil { if err != nil {
return time.Time{}, fmt.Errorf("%q: %s: %w", name, err, ParseFailed) return time.Time{}, fmt.Errorf("%q: %s: %w", name, err, ParseFailed)
@ -189,7 +246,7 @@ func ParseDatePageName(name string) (time.Time, error) {
if err != nil { if err != nil {
return time.Time{}, fmt.Errorf("%q: %s: %w", name, err, ParseFailed) return time.Time{}, fmt.Errorf("%q: %s: %w", name, err, ParseFailed)
} }
return time.Date(year, month, day, 0, 0, 0, 0, time.Local), nil return time.Date(year, month, day, 0, 0, 0, 0, time.UTC), nil
} }
return time.Time{}, fmt.Errorf("%q: invalid syntax: %w", name, ParseFailed) return time.Time{}, fmt.Errorf("%q: invalid syntax: %w", name, ParseFailed)
} }