@kobalab/liulian 0.7.4 → 0.8.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/ChangeLog.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## v0.8.0 / 2021-12-30
2
+
3
+ - #4 自動バックアップの機能を追加
4
+ - redirect モジュールを追加
5
+ - アイコンを変更
6
+ - include モジュールの再帰的使用を検知できるようにした
7
+ - HEADとTAILを二重インクルードしないよう修正
8
+
9
+ ### v0.7.7 / 2021-12-03
10
+
11
+ - | のみの行があると異常終了するバグを修正
12
+ - スマートフォンサイズ以外でも表があふれた場合は横スクロールするように修正
13
+
14
+ ### v0.7.6 / 2021-11-11
15
+
16
+ - 表のセルにclassが指定できるようにした
17
+ - 脆弱性警告に対処(debug 4.1.1 → 4.3.2)
18
+
19
+ ### v0.7.5 / 2021-10-30
20
+
21
+ - 脆弱性警告に対処(mocha 8.1.0 → 9.1.3)
22
+
1
23
  ### v0.7.4 / 2021-10-21
2
24
 
3
25
  - 脆弱性警告に対処(path-parse 1.0.6 → 1.0.7、glob-parent 5.1.1 → 5.1.2)
package/README.md CHANGED
@@ -3,6 +3,9 @@
3
3
  <a href="https://kobalab.net/xiumai/"><img src="https://kobalab.net/xiumai/theme/xiumai.png" alt="XiuMai" height=24 valign=bottom></a>
4
4
  の後継。Node.jsで動作するWebサイト作成ツール。
5
5
 
6
+ ## デモ
7
+ https://kobalab.net/liulian/
8
+
6
9
  ## インストール
7
10
  ```sh
8
11
  $ npm i -g @kobalab/liulian
@@ -33,6 +36,7 @@ $ liulian ~/Documents/LiuLian
33
36
  - [README と index](https://kobalab.net/liulian/man/readme&index)
34
37
  - [HEAD と TAIL](https://kobalab.net/liulian/man/head&tail)
35
38
  - [リバースプロキシを使う](https://kobalab.net/liulian/man/proxy-setting)
39
+ - [Gitで自動バックアップする](https://kobalab.net/liulian/man/git)
36
40
 
37
41
  ## ライセンス
38
42
  [MIT](https://github.com/kobalab/LiuLian/blob/master/LICENSE)
package/bin/liulian.js CHANGED
@@ -17,6 +17,8 @@ const mount = argv.mount;
17
17
 
18
18
  require('../lib/setup')(home);
19
19
 
20
+ const backup = require('../lib/backup/git')(path.join(home, 'docs'));
21
+
20
22
  const locale = require('../lib/util/locale')(
21
23
  path.join(__dirname, '../locale'),
22
24
  'en');
@@ -44,7 +46,8 @@ const liulian = require('../lib/liulian')({
44
46
  locale: locale,
45
47
  mount: mount,
46
48
  auth: auth,
47
- passport: passport });
49
+ passport: passport,
50
+ backup: backup });
48
51
 
49
52
  const app = express();
50
53
 
package/css/icon.png CHANGED
Binary file
package/css/liulian.css CHANGED
@@ -74,11 +74,6 @@ h6 {
74
74
  margin-left: 0;
75
75
  padding-left: 1.5em;
76
76
  }
77
-
78
- table {
79
- display: block;
80
- overflow: auto;
81
- }
82
77
  }
83
78
 
84
79
  hr {
@@ -100,7 +95,7 @@ pre {
100
95
  background: #eee;
101
96
  overflow: auto;
102
97
  overflow-wrap: normal;
103
- font-family: Osaka-Mono, "MS ゴシック", Courier, monospace;
98
+ font-family: Osaka-Mono, "HGゴシックM", "MS ゴシック", Courier, monospace;
104
99
  font-size: 100%;
105
100
  }
106
101
 
@@ -113,6 +108,8 @@ blockquote {
113
108
 
114
109
  table {
115
110
  border-collapse: collapse;
111
+ display: block;
112
+ overflow: auto;
116
113
  }
117
114
  th, td {
118
115
  border: solid 1px #9c9;
@@ -154,7 +151,7 @@ input[disabled] {
154
151
  }
155
152
  textarea {
156
153
  padding: 4px;
157
- font-family: Osaka-Mono, "MS ゴシック", Courier, monospace;
154
+ font-family: Osaka-Mono, "HGゴシックM", "MS ゴシック", Courier, monospace;
158
155
  font-size: 100%;
159
156
  border: solid 1px #999;
160
157
  border-radius: 4px;
@@ -299,6 +296,38 @@ input[type="submit"] {
299
296
  width: 100%;
300
297
  height: 70vh;
301
298
  }
299
+ #l-log .l-time {
300
+ text-align: right;
301
+ }
302
+ #l-diff {
303
+ border-radius: 0.2em;
304
+ border: solid 1px #080;
305
+ padding: 0;
306
+ background: #ddd;
307
+ }
308
+ #l-diff > * {
309
+ display: block;
310
+ padding: 2px 0.5em;
311
+ margin: 1px 0;
312
+ min-height: 1em;
313
+ font-family: Osaka-Mono, "MS ゴシック", Courier, monospace;
314
+ text-decoration: none;
315
+ white-space: pre-wrap;
316
+ color: #666;
317
+ background: #ffe;
318
+ }
319
+ #l-diff .l-head {
320
+ color: #999;
321
+ background: #cdf;
322
+ }
323
+ #l-diff ins {
324
+ color: #393;
325
+ background: #dfd;
326
+ }
327
+ #l-diff del {
328
+ color: #c33;
329
+ background: #fdd;
330
+ }
302
331
 
303
332
  .l-error {
304
333
  color: red;
@@ -0,0 +1,96 @@
1
+ /*
2
+ * backup/git
3
+ */
4
+ "use strict";
5
+
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const execSync = require('child_process').execFileSync;
9
+ const exec = require('util').promisify(require('child_process').execFile);
10
+ const spawn = require('child_process').spawn;
11
+
12
+ class Git {
13
+
14
+ constructor(repodir) {
15
+ let git_dir = path.join(repodir, '.git');
16
+ this._option = [ '--git-dir=' + git_dir, '--work-tree=' + repodir ];
17
+ fs.statSync(git_dir);
18
+ execSync('git', this._option.concat('init'));
19
+ try {
20
+ execSync('git', this._option.concat('log'));
21
+ }
22
+ catch(e) {
23
+ execSync('git', this._option.concat([
24
+ 'commit','--allow-empty','-m','.']));
25
+ }
26
+ }
27
+
28
+ async checkIn(location, time, user) {
29
+ let path = location.replace(/^\//,'');
30
+ let log = await exec('git', this._option.concat([
31
+ 'log','--oneline','--', path]));
32
+ let diff = await exec('git', this._option.concat(['diff','--', path]));
33
+ if (! log.stdout || diff.stdout) {
34
+ await exec('git', this._option.concat(['add', path]));
35
+ await exec('git', this._option.concat([
36
+ 'commit','-m', `${time}:${user}`]));
37
+ }
38
+ }
39
+
40
+ async checkOut(location, rev) {
41
+ let path = location.replace(/^\//,'');
42
+ let rv = await exec('git', this._option.concat([
43
+ 'show',`${rev}:${path}`]));
44
+ return rv.stdout;
45
+ }
46
+
47
+ checkOutStream(location, rev) {
48
+ let path = location.replace(/^\//,'');
49
+ let rv = spawn('git', this._option.concat([
50
+ 'show',`${rev}:${path}`]));
51
+ return rv.stdout;
52
+ }
53
+
54
+ async log(location) {
55
+ let path = location.replace(/^\//,'');
56
+ let rv = await exec('git', this._option.concat([
57
+ 'log','--pretty=format:%h\t%s','--', path]));
58
+ let log = [];
59
+ if (! rv.stdout) return log;
60
+ for (let line of rv.stdout.split(/\n/)) {
61
+ let [rev, comment] = line.split(/\t/);
62
+ let [time, user] = comment.split(/:/);
63
+ log.push({ rev: rev, time: +time, user: user });
64
+ }
65
+ return log;
66
+ }
67
+
68
+ async diff(location, rev1, rev2) {
69
+ let rv;
70
+ let path = location.replace(/^\//,'');
71
+ if (rev2) {
72
+ rv = await exec('git', this._option.concat([
73
+ 'diff',`${rev1}..${rev2}`,'--', path]));
74
+ }
75
+ else {
76
+ rv = await exec('git', this._option.concat([
77
+ 'diff', rev1, '--', path]));
78
+ }
79
+ let diff = [], body;
80
+ for (let line of rv.stdout.split(/\n/)) {
81
+ if (line.match(/^@@/)) body = 1;
82
+ if (! body) continue;
83
+ diff.push(line);
84
+ }
85
+ return diff;
86
+ }
87
+ }
88
+
89
+ module.exports = function(repodir) {
90
+ try {
91
+ return new Git(repodir);
92
+ }
93
+ catch(e) {
94
+ return;
95
+ }
96
+ }
package/lib/html/file.js CHANGED
@@ -4,6 +4,7 @@
4
4
  "use strict";
5
5
 
6
6
  const { cdata, fixpath } = require('../util/html-escape');
7
+ const { timeStr } = require('../util/str-tool');
7
8
 
8
9
  const HTML = require('./');
9
10
 
@@ -15,19 +16,40 @@ module.exports = class File extends HTML {
15
16
  }
16
17
 
17
18
  _editmenu() {
19
+ let path = fixpath(this._r.location, this._req.baseUrl);
20
+ let rev = this._req.param('rev');
21
+ let param = this._req.cmd == 'edit' ? ''
22
+ : this._req.cmd == 'diff' ? '?cmd=edit'
23
+ : rev ? `?cmd=edit&rev=${rev}`
24
+ : '?cmd=edit';
25
+
26
+ return `<li><a href="${cdata(path + param)}" accesskey="e">`
27
+ + cdata(this.msg('toolbar.edit'))
28
+ + `</a></li>\n`;
29
+ }
30
+
31
+ _logmenu() {
32
+ if (! this._r._backup) return ''
18
33
  let path = fixpath(this._r.location, this._req.baseUrl);
19
34
  return '<li><a href="'
20
35
  + cdata(path)
21
- + (this._req.cmd == 'edit' ? '' : '?cmd=edit')
22
- + '" accesskey="e">'
23
- + cdata(this.msg('toolbar.edit'))
36
+ + (this._req.cmd == 'log' ? '' : '?cmd=log')
37
+ + '">'
38
+ + cdata(this.msg('toolbar.log'))
24
39
  + `</a></li>\n`;
25
40
  }
26
41
 
27
42
  _title() {
28
- this.title(this._r.name);
43
+ const req = this._req;
44
+ const msg = req.msg;
45
+
46
+ let title = this._r.name;
47
+ if (req.cmd == 'log') title = msg('log.title', title);
48
+ else if (req.cmd == 'diff') title = msg('diff.title', title);
49
+
50
+ this.title(title);
29
51
  return `<h1 id="l-title"><a href="${cdata(this._r.name)}">`
30
- + `${cdata(this._r.name)}</a></h1>\n`;
52
+ + `${cdata(title)}</a></h1>\n`;
31
53
  }
32
54
 
33
55
  _path() {
@@ -92,6 +114,50 @@ module.exports = class File extends HTML {
92
114
  + '</script>\n';
93
115
  }
94
116
 
117
+ _log() {
118
+
119
+ const msg = this._req.msg;
120
+
121
+ let html = '<table id="l-log">\n';
122
+ html += '<tr>'
123
+ + `<th>${cdata(msg('log.time'))}</th>`
124
+ + (this._r.text != null
125
+ ? `<th colspan="2">${cdata(msg('log.diff'))}</th>`
126
+ : '')
127
+ + '<th></th>'
128
+ + (this._r.text != null ? '<th></th>' : '')
129
+ + '</tr>\n'
130
+
131
+ for (let i = 0; i < this._r.log.length; i++) {
132
+
133
+ let log = this._r.log[i];
134
+ let log2 = this._r.log[i + 1];
135
+
136
+ html += '<tr>'
137
+ + `<td class="l-time">${timeStr(log.time)}</td>`
138
+ + (this._r.text != null
139
+ ? ((log2 ? (
140
+ '<td class="l-diff">'
141
+ + `<a href="${cdata(
142
+ `?cmd=diff&rev=${log2.rev}&rev=${log.rev}`)}">`
143
+ + `${cdata(msg('log.prev'))}</a></td>`
144
+ ) : '<td></td>')
145
+ + '<td class="l-diff">'
146
+ + `<a href="${cdata(`?cmd=diff&rev=${log.rev}`)}">`
147
+ + `${cdata(msg('log.curr'))}</a></td>`)
148
+ : '')
149
+ + `<td><a href="?rev=${log.rev}">`
150
+ + `${cdata(msg('log.show'))}</a></td>`
151
+ + (this._r.text != null
152
+ ? `<td><a href="?cmd=edit&rev=${log.rev}">`
153
+ + `${cdata(msg('log.edit'))}</a></td>`
154
+ : '')
155
+ + '</tr>\n';
156
+ }
157
+
158
+ return html += '</table>\n';
159
+ }
160
+
95
161
  edit() {
96
162
  return this.stringify(
97
163
  this._title()
@@ -99,4 +165,11 @@ module.exports = class File extends HTML {
99
165
  + this._form()
100
166
  );
101
167
  }
168
+
169
+ log() {
170
+ return this.stringify(
171
+ this._title()
172
+ + this._log()
173
+ );
174
+ }
102
175
  }
@@ -4,39 +4,8 @@
4
4
  "use strict";
5
5
 
6
6
  const File = require('./file');
7
- const { cdata, fixpath } = require('../util/html-escape');
8
-
9
- function timeStr(time) {
10
-
11
- const now = new Date().getTime();
12
-
13
- const date = new Date(time);
14
- const year = date.getFullYear();
15
- const m = date.getMonth() + 1;
16
- const mm = ('0' + m).substr(-2);
17
- const d = date.getDate();
18
- const dd = ('0' + d).substr(-2);
19
- const hour = ('0' + date.getHours()).substr(-2);
20
- const min = ('0' + date.getMinutes()).substr(-2);
21
-
22
- if (now - time < 1000*60*60*12) return `${hour}:${min}`;
23
- else if (now - time < 1000*60*60*24*365/2)
24
- return `${m}/${d} ${hour}:${min}`;
25
- else return `${year}/${mm}/${dd} `;
26
- }
27
-
28
- function sizeStr(size) {
29
-
30
- if (size == null) return '-';
31
-
32
- let str;
33
- for (let unit of ['',' KB',' MB',' GB',' TB']) {
34
- str = unit ? size.toFixed(1) + unit : size + unit;
35
- if (size < 1024) return str;
36
- size = size / 1024;
37
- }
38
- return str;
39
- }
7
+ const { cdata, fixpath } = require('../util/html-escape');
8
+ const { timeStr, sizeStr } = require('../util/str-tool');
40
9
 
41
10
  function cmp(key) {
42
11
  return key == 'n' ? (x, y)=> ! x.type && y.type ? -1
@@ -94,6 +63,8 @@ function uc(c) { return c.toUpperCase() }
94
63
 
95
64
  module.exports = class Folder extends File {
96
65
 
66
+ _logmenu() { return '' }
67
+
97
68
  _table() {
98
69
 
99
70
  const req = this._req;
package/lib/html/index.js CHANGED
@@ -18,6 +18,7 @@ module.exports = class HTML {
18
18
  style: '',
19
19
  icon: DEFAULT_ICON,
20
20
  script: [],
21
+ meta: [],
21
22
  };
22
23
  this.msg = req.msg;
23
24
  }
@@ -58,6 +59,12 @@ module.exports = class HTML {
58
59
  return this;
59
60
  }
60
61
 
62
+ meta(attr) {
63
+ if (attr) this._.meta.push(attr);
64
+ else return this._.meta;
65
+ return this;
66
+ }
67
+
61
68
  _head() {
62
69
  const req = this._req;
63
70
 
@@ -83,10 +90,17 @@ module.exports = class HTML {
83
90
  `<script>\n${opt.code}</script>\n`
84
91
  ).join('');
85
92
 
93
+ const meta = this._.meta.map(attr=>{
94
+ let attrs = Object.keys(attr).map(key=>
95
+ `${cdata(key)}="${cdata(attr[key])}"`).join(' ');
96
+ return `<meta ${attrs}>\n`;
97
+ }).join('');
98
+
86
99
  return '<head>\n'
87
100
  + '<meta charset="utf-8">\n'
88
101
  + '<meta name="viewport" '
89
102
  + 'content="width=device-width, initial-scale=1">\n'
103
+ + meta
90
104
  + `<title>${cref(this.title())}</title>\n`
91
105
  + scriptRef
92
106
  + icon
@@ -98,6 +112,8 @@ module.exports = class HTML {
98
112
 
99
113
  _editmenu() { return '' }
100
114
 
115
+ _logmenu() { return '' }
116
+
101
117
  _toolbar() {
102
118
  const msg = this.msg;
103
119
  const req = this._req;
@@ -109,6 +125,7 @@ module.exports = class HTML {
109
125
  + ` alt="${cdata(msg('toolbar.home'))}"></a>\n`
110
126
  + `<ul>\n`;
111
127
  if (this._req.user) {
128
+ html += this._logmenu();
112
129
  html += this._editmenu();
113
130
  const url = '?cmd=logout&session_id='
114
131
  + encodeURIComponent(this._req.sessionID);
package/lib/html/text.js CHANGED
@@ -20,4 +20,34 @@ module.exports = class Text extends File {
20
20
  + `<input type="submit" value="${cdata(msg('udtext.submit'))}">\n`
21
21
  + '</form>\n';
22
22
  }
23
+
24
+ _diff() {
25
+
26
+ if (! this._r.diff.length) return '';
27
+
28
+ let html = '<div id="l-diff">\n';
29
+
30
+ for (let line of this._r.diff) {
31
+
32
+ if (! line.length) continue;
33
+ if (line[0] == '\\') continue;
34
+
35
+ html += line[0] == '@'
36
+ ? `<span class="l-head">${cdata(line)}</span>\n`
37
+ : line[0] == '+'
38
+ ? `<ins>${cdata(line.substr(1))}</ins>\n`
39
+ : line[0] == '-'
40
+ ? `<del>${cdata(line.substr(1))}</del>\n`
41
+ : `<span>${cdata(line.substr(1))}</span>\n`;
42
+ }
43
+
44
+ return html += '</div>\n';
45
+ }
46
+
47
+ diff() {
48
+ return this.stringify(
49
+ this._title()
50
+ + this._diff()
51
+ );
52
+ }
23
53
  }
@@ -15,6 +15,7 @@ module.exports = class Request {
15
15
  this._liulian = liulian;
16
16
  this._req = req;
17
17
  this.msg = liulian._.locale(req.acceptsLanguages());
18
+ this._n_open = 0;
18
19
  }
19
20
 
20
21
  get version() { return this._liulian._version }
@@ -62,4 +63,9 @@ module.exports = class Request {
62
63
  : isAbsolute(path) ? base + path
63
64
  : base + join(this.pathDir, path);
64
65
  }
66
+
67
+ openFile() {
68
+ this._n_open++;
69
+ if (this._n_open > 1000) throw 508;
70
+ }
65
71
  }
@@ -39,6 +39,12 @@ module.exports = class Response {
39
39
  }
40
40
  }
41
41
 
42
+ sendStream(stream, type) {
43
+ this._res.type(type)
44
+ stream.pipe(this._res);
45
+ this._cleanUp();
46
+ }
47
+
42
48
  sendText(text, type = 'text/html') {
43
49
  this._res.type(type).send(text);
44
50
  this._cleanUp();
@@ -13,7 +13,7 @@ module.exports = class Core {
13
13
  this._req = parser._r._req;
14
14
  this._inline = ['img','color','size','br','clear','class','lang'];
15
15
  this._block = ['title','contents','footnote','include','img','clear',
16
- 'class','style','icon','lang','script'];
16
+ 'class','style','icon','lang','redirect','script'];
17
17
  this._np = ['style','script'];
18
18
  }
19
19
 
@@ -148,6 +148,17 @@ module.exports = class Core {
148
148
  return '';
149
149
  }
150
150
 
151
+ redirect(type, param, value) {
152
+ let [url, sec] = param.split(/,\s*/);
153
+ sec = sec || '0';
154
+ let attr = {
155
+ 'http-equiv': 'refresh',
156
+ 'content': `${cdata(sec)}; URL=${cdata(url)}`
157
+ };
158
+ this._r.meta(attr);
159
+ return '';
160
+ }
161
+
151
162
  script(type, param, value) {
152
163
  this._r.script({ url: param, code: value });
153
164
  return '';
@@ -17,6 +17,7 @@ module.exports = class File {
17
17
  this._stat = stat;
18
18
  this._location = location;
19
19
  this.openFile = openFile;
20
+ this._backup = req.config.backup;
20
21
  }
21
22
 
22
23
  get path() { return this._path }
@@ -34,20 +35,32 @@ module.exports = class File {
34
35
  return this._type;
35
36
  }
36
37
  get location() { return this._location }
37
-
38
38
  get redirect() { return this._redirect }
39
+ get log() { return this._log }
39
40
 
40
41
  async open() {
41
- if (this._req.cmd == 'edit' && ! this._req.user) throw 403;
42
-
42
+ if (! this._req.user && (this._req.cmd == 'edit' ||
43
+ this._req.cmd == 'log' ||
44
+ this._req.cmd == 'diff' )) throw 403;
43
45
  try {
44
46
  const fh = await fs.open(this._path);
45
47
  fh.close();
46
- return this;
47
48
  }
48
49
  catch(err) {
49
50
  throw 403;
50
51
  }
52
+ if (this._backup && (this._req.cmd == 'edit' ||
53
+ this._req.cmd == 'log'))
54
+ {
55
+ if (Date.now() - this.time > 1000*60*60*24) {
56
+ await this._backup.checkIn(this.location,
57
+ this.time, this._req.user);
58
+ }
59
+ }
60
+ if (this._backup && this._req.cmd == 'log') {
61
+ this._log = await this._backup.log(this.location);
62
+ }
63
+ return this;
51
64
  }
52
65
 
53
66
  async update() {
@@ -66,8 +79,20 @@ module.exports = class File {
66
79
  }
67
80
 
68
81
  send(res) {
69
- if (this._req.cmd == 'edit')
70
- res.sendText(new HTML(this).edit());
71
- else res.sendFile(this._path, this.type);
82
+ if (this._req.cmd == 'edit') res.sendText(new HTML(this).edit());
83
+ else if (this._req.cmd == 'log') res.sendText(new HTML(this).log());
84
+ else {
85
+ try {
86
+ let rev = this._req.param('rev');
87
+ if (rev) {
88
+ res.sendStream(
89
+ this._backup.checkOutStream(this.location, rev),
90
+ this.type);
91
+ return;
92
+ }
93
+ }
94
+ catch (err) {}
95
+ res.sendFile(this._path, this.type);
96
+ }
72
97
  }
73
98
  }
@@ -21,7 +21,11 @@ async function resource(req, file) {
21
21
  else location = decodeURIComponent(urljoin(req.pathDir, file));
22
22
  }
23
23
  else location = decodeURIComponent(req.path);
24
+
25
+ if (location.match(/^\/\.git(?:\/.*)?$/)) throw 404;
26
+
24
27
  const path = join(req.config.home, '/docs/', location);
28
+ req.openFile();
25
29
 
26
30
  try {
27
31
  const stat = await fs.stat(path);
@@ -16,6 +16,7 @@ module.exports = class LiuLian extends Text {
16
16
  icon(url) { this._html.icon(url) }
17
17
  lang(lang) { this._html.lang(lang) }
18
18
  script(script) { this._html.script(script) }
19
+ meta(attr) { this._html.meta(attr) }
19
20
 
20
21
  async _seekToParent(filename) {
21
22
  let pathDir = this._req.pathDir;
@@ -41,10 +42,18 @@ module.exports = class LiuLian extends Text {
41
42
  if (this._req.cmd == 'edit') {
42
43
  res.sendText(new HTML(this).edit());
43
44
  }
45
+ else if (this._req.cmd == 'log') {
46
+ res.sendText(new HTML(this).log());
47
+ }
48
+ else if (this._req.cmd == 'diff') {
49
+ res.sendText(new HTML(this).diff());
50
+ }
44
51
  else {
45
- this._text = await this._seekToParent('HEAD') + '\n'
52
+ this._text = (this.name != 'HEAD'
53
+ ? await this._seekToParent('HEAD') + '\n' : '')
46
54
  + this._text + '\n'
47
- + await this._seekToParent('TAIL');
55
+ + (this.name != 'TAIL'
56
+ ? await this._seekToParent('TAIL') : '');
48
57
  this._html = new HTML(this);
49
58
  res.sendText(this._html.stringify(await parse(this)));
50
59
  }
@@ -11,17 +11,32 @@ const File = require('./file');
11
11
  module.exports = class Text extends File {
12
12
 
13
13
  get text() { return this._text }
14
+ get diff() { return this._diff }
14
15
 
15
16
  async open() {
16
- if (this._req.cmd == 'edit' && ! this._req.user) throw 403;
17
-
17
+ await super.open();
18
+ if (this._backup && this._req.cmd == 'diff') {
19
+ let rev = this._req.params('rev');
20
+ this._diff = await this._backup.diff(this.location, ...rev);
21
+ return this;
22
+ }
23
+ try {
24
+ let rev = this._req.param('rev');
25
+ if (rev) {
26
+ this._text = await this._backup.checkOut(this.location, rev);
27
+ return this;
28
+ }
29
+ }
30
+ catch(err) {
31
+ throw 404;
32
+ }
18
33
  try {
19
34
  this._text = await fs.readFile(this._path, 'utf-8');
20
- return this;
21
35
  }
22
36
  catch(err) {
23
37
  throw 403;
24
38
  }
39
+ return this;
25
40
  }
26
41
 
27
42
  async update() {
@@ -39,8 +54,9 @@ module.exports = class Text extends File {
39
54
  }
40
55
 
41
56
  send(res) {
42
- if (this._req.cmd == 'edit')
43
- res.sendText(new HTML(this).edit());
44
- else res.sendFile(this._path, this.type);
57
+ if (this._req.cmd == 'edit') res.sendText(new HTML(this).edit());
58
+ else if (this._req.cmd == 'log') res.sendText(new HTML(this).log());
59
+ else if (this._req.cmd == 'diff') res.sendText(new HTML(this).diff());
60
+ else res.sendFile(this._path, this.type);
45
61
  }
46
62
  }
@@ -229,7 +229,7 @@ class LiuLian {
229
229
  let cell = [];
230
230
  for (;;) {
231
231
  let line = this.readline().replace(/\|$/,'');
232
- let cols = line.match(/\|(?:\[\[.*?\]\]|[^\|])*/g)
232
+ let cols = line.match(/\|(?:\[\[.*?\]\]|[^\|])*/g) || [];
233
233
  cell.push(cols.map(c=>c.replace(/^\|/,'')));
234
234
  line = this.nextline();
235
235
  if (! line || ! line.match(/^\|/)) break;
@@ -269,13 +269,19 @@ class LiuLian {
269
269
  if (align) style.push('text-align:' + align);
270
270
  text = text.replace(/^[<=>]/,'');
271
271
 
272
+ let cls = (text.match(/^(?:\.[\w\-]+)+/)||[])[0];
273
+ text = text.replace(/^(?:\.[\w\-]+)+/,'');
274
+ cls = cls
275
+ ? ` class="${cls.replace(/^\./,'').replace(/\./g,' ')}"`
276
+ : '';
277
+
272
278
  let color = (text.match(/^#[0-9a-f]+/i)||[])[0];
273
279
  if (color) style.push('background:' + color);
274
280
  text = text.replace(/^#[0-9a-f]+/i,'');
275
281
 
276
282
  if (style.length) style = ` style="${cdata(style.join(';'))}"`;
277
283
 
278
- html += '<' + hd + colspan + rowspan + style + '>'
284
+ html += '<' + hd + colspan + rowspan + cls + style + '>'
279
285
  + this.inline(text) + '</' + hd + '>';
280
286
  }
281
287
  html += '</tr>\n';
@@ -0,0 +1,40 @@
1
+ /*
2
+ * util/str-tool
3
+ */
4
+
5
+ function timeStr(time) {
6
+
7
+ const now = new Date().getTime();
8
+
9
+ const date = new Date(time);
10
+ const year = date.getFullYear();
11
+ const m = date.getMonth() + 1;
12
+ const mm = ('0' + m).substr(-2);
13
+ const d = date.getDate();
14
+ const dd = ('0' + d).substr(-2);
15
+ const hour = ('0' + date.getHours()).substr(-2);
16
+ const min = ('0' + date.getMinutes()).substr(-2);
17
+
18
+ if (now - time < 1000*60*60*12) return `${hour}:${min}`;
19
+ else if (now - time < 1000*60*60*24*365/2)
20
+ return `${m}/${d} ${hour}:${min}`;
21
+ else return `${year}/${mm}/${dd}`;
22
+ }
23
+
24
+ function sizeStr(size) {
25
+
26
+ if (size == null) return '-';
27
+
28
+ let str;
29
+ for (let unit of ['',' KB',' MB',' GB',' TB']) {
30
+ str = unit ? size.toFixed(1) + unit : size + unit;
31
+ if (size < 1024) return str;
32
+ size = size / 1024;
33
+ }
34
+ return str;
35
+ }
36
+
37
+ module.exports = {
38
+ timeStr: timeStr,
39
+ sizeStr: sizeStr,
40
+ };
package/locale/en CHANGED
@@ -13,6 +13,7 @@ toolbar.home:Home
13
13
  toolbar.login:Login
14
14
  toolbar.logout:Logout
15
15
  toolbar.edit:Edit
16
+ toolbar.log:History
16
17
  # -----------------------------------------------------------------------------
17
18
  adduser.title:Sign Up
18
19
  adduser.user:Login Name
@@ -45,3 +46,12 @@ rmdir.submit:REMOVE
45
46
  # -----------------------------------------------------------------------------
46
47
  udtext.submit:SAVE
47
48
  # -----------------------------------------------------------------------------
49
+ log.title:Revision history of {$1}
50
+ log.rev:Revision
51
+ log.time:Modified
52
+ log.diff:Difference
53
+ log.prev:from previous
54
+ log.curr:to current
55
+ log.show:Show
56
+ log.edit:Edit
57
+ diff.title:Difference of {$1}
package/locale/ja CHANGED
@@ -13,6 +13,7 @@ toolbar.home:ホーム
13
13
  toolbar.login:ログイン
14
14
  toolbar.logout:ログアウト
15
15
  toolbar.edit:編集
16
+ toolbar.log:履歴
16
17
  # -----------------------------------------------------------------------------
17
18
  adduser.title:ユーザー登録
18
19
  adduser.user:ログイン名
@@ -45,3 +46,12 @@ rmdir.submit:削除
45
46
  # -----------------------------------------------------------------------------
46
47
  udtext.submit:保存
47
48
  # -----------------------------------------------------------------------------
49
+ log.title:{$1} の変更履歴
50
+ log.rev:リビジョン
51
+ log.time:変更日
52
+ log.diff:差分
53
+ log.prev:前の版との差分
54
+ log.curr:現在との差分
55
+ log.show:表示
56
+ log.edit:編集
57
+ diff.title:{$1} の差分
package/locale/zh-CN CHANGED
@@ -13,6 +13,7 @@ toolbar.home:主页
13
13
  toolbar.login:登录
14
14
  toolbar.logout:注销
15
15
  toolbar.edit:编辑
16
+ toolbar.log:历史
16
17
  # -----------------------------------------------------------------------------
17
18
  adduser.title:注册
18
19
  adduser.user:账号
@@ -45,3 +46,12 @@ rmdir.submit:删除
45
46
  # -----------------------------------------------------------------------------
46
47
  udtext.submit:保存
47
48
  # -----------------------------------------------------------------------------
49
+ log.title:{$1} 的历史纪录
50
+ log.rev:版本
51
+ log.time:修改日期
52
+ log.diff:差别
53
+ log.prev:从前版的
54
+ log.curr:到现在的
55
+ log.show:表示这版
56
+ log.edit:编辑这版
57
+ diff.title:{$1} 的差别
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kobalab/liulian",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "Node.jsで動作するWebサイト作成ツール",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://kobalab.net/liulian/",
29
29
  "devDependencies": {
30
- "mocha": "^8.1.0",
30
+ "mocha": "^9.1.3",
31
31
  "nyc": "^15.1.0"
32
32
  },
33
33
  "dependencies": {