margins 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/bin/margins.js +107 -0
- package/lib/browser.js +44 -0
- package/lib/cli.js +86 -0
- package/lib/files.js +194 -0
- package/lib/links.js +314 -0
- package/lib/paths.js +159 -0
- package/lib/search.js +117 -0
- package/lib/security.js +89 -0
- package/lib/tree.js +126 -0
- package/package.json +51 -0
- package/public/app.js +1222 -0
- package/public/favicon.svg +1 -0
- package/public/index.html +119 -0
- package/public/style.css +781 -0
- package/server.js +325 -0
package/lib/tree.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { PROTECTED_DIRS, isInside, toRel } = require('./paths');
|
|
7
|
+
const { kindOf } = require('./files');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What a folder contains, for the sidebar, and every file below it, for
|
|
11
|
+
* quick open, search and backlinks.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// A folder of notes has hundreds of files; a repository opened by accident
|
|
15
|
+
// can have hundreds of thousands. Past this, walking stops and says so,
|
|
16
|
+
// rather than making the page wait on a scan of someone's home directory.
|
|
17
|
+
const WALK_LIMIT = 20000;
|
|
18
|
+
|
|
19
|
+
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
20
|
+
|
|
21
|
+
/** Whether a name is hidden from listings unless hidden files were asked for. */
|
|
22
|
+
function isHiddenName(name) {
|
|
23
|
+
return name.startsWith('.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a directory entry to what it really is, following one symlink and
|
|
28
|
+
* dropping anything that leads outside the root -- a sidebar should not list
|
|
29
|
+
* what margins would then refuse to open.
|
|
30
|
+
*
|
|
31
|
+
* @returns {Promise<'dir'|'file'|null>}
|
|
32
|
+
*/
|
|
33
|
+
async function entryType(realRoot, abs, dirent) {
|
|
34
|
+
if (dirent.isDirectory()) return 'dir';
|
|
35
|
+
if (dirent.isFile()) return 'file';
|
|
36
|
+
if (!dirent.isSymbolicLink()) return null;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const real = await fs.realpath(abs);
|
|
40
|
+
if (!isInside(realRoot, real)) return null;
|
|
41
|
+
const stat = await fs.stat(real);
|
|
42
|
+
return stat.isDirectory() ? 'dir' : stat.isFile() ? 'file' : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null; // a dangling link
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The entries of one folder: folders first, then files, each in the order a
|
|
50
|
+
* person sorts them -- "note 2" before "note 10".
|
|
51
|
+
*
|
|
52
|
+
* @param {string} realRoot
|
|
53
|
+
* @param {string} absDir already confined to the root
|
|
54
|
+
* @param {{hidden?: boolean}} [options]
|
|
55
|
+
*/
|
|
56
|
+
async function listDir(realRoot, absDir, { hidden = false } = {}) {
|
|
57
|
+
const dirents = await fs.readdir(absDir, { withFileTypes: true });
|
|
58
|
+
const entries = [];
|
|
59
|
+
|
|
60
|
+
for (const dirent of dirents) {
|
|
61
|
+
if (PROTECTED_DIRS.has(dirent.name)) continue;
|
|
62
|
+
if (!hidden && isHiddenName(dirent.name)) continue;
|
|
63
|
+
|
|
64
|
+
const abs = path.join(absDir, dirent.name);
|
|
65
|
+
const type = await entryType(realRoot, abs, dirent);
|
|
66
|
+
if (!type) continue;
|
|
67
|
+
|
|
68
|
+
entries.push({
|
|
69
|
+
name: dirent.name,
|
|
70
|
+
path: toRel(realRoot, abs),
|
|
71
|
+
type,
|
|
72
|
+
...(type === 'file' ? { kind: kindOf(dirent.name) } : {})
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return entries.sort((a, b) => {
|
|
77
|
+
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
|
|
78
|
+
return collator.compare(a.name, b.name);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Every file below the root, depth first, skipping protected and (unless
|
|
84
|
+
* asked) hidden folders.
|
|
85
|
+
*
|
|
86
|
+
* Symlinked folders are not followed. A link back up the tree is a loop, and
|
|
87
|
+
* a link sideways into a large tree is a surprise; the sidebar still shows
|
|
88
|
+
* them, and opening one lists it on demand.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} realRoot
|
|
91
|
+
* @param {{hidden?: boolean, limit?: number}} [options]
|
|
92
|
+
* @returns {Promise<{files: {path: string, kind: string}[], truncated: boolean}>}
|
|
93
|
+
*/
|
|
94
|
+
async function walkFiles(realRoot, { hidden = false, limit = WALK_LIMIT } = {}) {
|
|
95
|
+
const files = [];
|
|
96
|
+
const pending = [realRoot];
|
|
97
|
+
|
|
98
|
+
while (pending.length > 0) {
|
|
99
|
+
const dir = pending.pop();
|
|
100
|
+
let dirents;
|
|
101
|
+
try {
|
|
102
|
+
dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
103
|
+
} catch {
|
|
104
|
+
continue; // unreadable: skip it, do not fail the whole walk
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
dirents.sort((a, b) => collator.compare(a.name, b.name));
|
|
108
|
+
for (const dirent of dirents) {
|
|
109
|
+
if (PROTECTED_DIRS.has(dirent.name)) continue;
|
|
110
|
+
if (!hidden && isHiddenName(dirent.name)) continue;
|
|
111
|
+
|
|
112
|
+
const abs = path.join(dir, dirent.name);
|
|
113
|
+
if (dirent.isDirectory()) {
|
|
114
|
+
pending.push(abs);
|
|
115
|
+
} else if (dirent.isFile()) {
|
|
116
|
+
files.push({ path: toRel(realRoot, abs), kind: kindOf(dirent.name) });
|
|
117
|
+
if (files.length >= limit) return { files, truncated: true };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
files.sort((a, b) => collator.compare(a.path, b.path));
|
|
123
|
+
return { files, truncated: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { WALK_LIMIT, isHiddenName, listDir, walkFiles };
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "margins",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open a folder of markdown in your browser from the terminal. Browse, read, edit, search, and follow [[wikilinks]] and backlinks. No vault, no config, nothing written but what you save.",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node bin/margins.js",
|
|
8
|
+
"test": "node --test",
|
|
9
|
+
"test:watch": "node --test --watch"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"markdown",
|
|
13
|
+
"notes",
|
|
14
|
+
"obsidian",
|
|
15
|
+
"wikilinks",
|
|
16
|
+
"backlinks",
|
|
17
|
+
"markdown-editor",
|
|
18
|
+
"markdown-viewer",
|
|
19
|
+
"docs",
|
|
20
|
+
"cli",
|
|
21
|
+
"local-first",
|
|
22
|
+
"knowledge-base",
|
|
23
|
+
"zettelkasten"
|
|
24
|
+
],
|
|
25
|
+
"author": "Dheeraj Jha",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"dompurify": "^3.4.16",
|
|
29
|
+
"marked": "^18.0.14"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"margins": "bin/margins.js"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin/",
|
|
36
|
+
"lib/",
|
|
37
|
+
"public/",
|
|
38
|
+
"server.js"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/dheerajjha/inkd.git"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/dheerajjha/inkd/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/dheerajjha/inkd#readme"
|
|
51
|
+
}
|