@kobalab/liulian 0.8.1 → 1.0.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,20 @@
1
+ # v1.0.0 / 2022-04-09
2
+
3
+ - 正式バージョンリリース
4
+ - #6 Markdown記法に対応した
5
+ - JavaScriptを編集可能にした
6
+
7
+ ### v0.9.1 / 2022-04-06
8
+
9
+ - paiga モジュールに空文字列を指定した場合に異常終了するバグを修正
10
+
11
+ ## v0.9.0 / 2022-04-03
12
+
13
+ - import モジュールで外部モジュールをインポートできるようにした
14
+ - pinyin モジュール(中国語のピンインを表示する)を追加
15
+ - paiga モジュール(麻雀の牌画像を表示する)を追加
16
+ - 脆弱性警告に対処(minimist 1.2.5 → 1.2.6)
17
+
1
18
  ### v0.8.1 / 2022-01-24
2
19
 
3
20
  - 脆弱性警告に対処(mocha 9.1.3 → 9.2.0)
package/css/liulian.css CHANGED
@@ -333,6 +333,13 @@ input[type="submit"] {
333
333
  color: red;
334
334
  }
335
335
 
336
+ .footnotes {
337
+ font-size: 90%;
338
+ }
339
+ .footnotes li p {
340
+ margin: 0.2em auto;
341
+ }
342
+
336
343
  .hljs-comment,
337
344
  .hljs-quote {
338
345
  color: #a0a1a7;
@@ -97,7 +97,7 @@ module.exports = class Folder extends File {
97
97
  for (let file of this._r.files.sort(cmp(s))) {
98
98
 
99
99
  if (req.cmd != 'edit'
100
- && file.name.match(/^(?:README|HEAD|TAIL)$/)) continue;
100
+ && file.name.match(/^(?:README|HEAD|TAIL)(?:\.md)?$/)) continue;
101
101
 
102
102
  let name = file.name + (file.type ? '' : '/');
103
103
  let link = encodeURIComponent(file.name) + (file.type ? '' : '/')
package/lib/html/index.js CHANGED
@@ -30,8 +30,8 @@ module.exports = class HTML {
30
30
  }
31
31
 
32
32
  title(title) {
33
- if (title) this._.title = title;
34
- else return this._.title;
33
+ if (title != null) this._.title = title;
34
+ else return this._.title;
35
35
  return this;
36
36
  }
37
37
 
@@ -68,8 +68,6 @@ module.exports = class HTML {
68
68
  _head() {
69
69
  const req = this._req;
70
70
 
71
- if (! this._.stylesheet.length)
72
- this._.stylesheet.push({url: DEFAULT_STYLE});
73
71
  const stylesheet = this._.stylesheet.map(opt=>
74
72
  '<link rel="stylesheet" type="text/css" '
75
73
  + `href="${cdata(fixpath(opt.url, req.baseUrl))}"`
@@ -151,7 +149,8 @@ module.exports = class HTML {
151
149
  + `</div>\n`;
152
150
  }
153
151
 
154
- stringify(content = '') {
152
+ stringify(content = '', needDefaultStyle = ! this._.stylesheet.length) {
153
+ if (needDefaultStyle) this._.stylesheet.push({url: DEFAULT_STYLE});
155
154
  return '<!DOCTYPE html>\n'
156
155
  + `<html lang="${cdata(this.lang())}">\n`
157
156
  + this._head()
@@ -15,16 +15,16 @@ module.exports = class Module {
15
15
  this.import('core');
16
16
  }
17
17
 
18
- require(path) {
19
- try { return require(path) }
20
- catch(err) { console.log(err) }
21
- }
22
-
23
18
  import(module) {
24
- if (! modules[module]) modules[module] = this.require('./' + module);
25
- if (modules[module]) this._modules.push(
26
- new modules[module](this._parser));
27
- return modules[module];
19
+ try {
20
+ if (! modules[module]) modules[module] = require('./' + module);
21
+ this._modules.push(new modules[module](this._parser));
22
+ return modules[module];
23
+ }
24
+ catch(err) {
25
+ if (err.code) console.log(module, err.code);
26
+ else console.log(err);
27
+ }
28
28
  }
29
29
 
30
30
  callInlineModule(str, name, param, value) {
@@ -0,0 +1,124 @@
1
+ /*
2
+ * module/paiga
3
+ */
4
+ "use strict";
5
+
6
+ const imgbase = '//kobalab.github.io/paiga/';
7
+ const img = {
8
+ _: 'ura.gif',
9
+
10
+ m0: 'man5red.gif',
11
+ m1: 'man1.gif', m2: 'man2.gif', m3: 'man3.gif',
12
+ m4: 'man4.gif', m5: 'man5.gif', m6: 'man6.gif',
13
+ m7: 'man7.gif', m8: 'man8.gif', m9: 'man9.gif',
14
+
15
+ p0: 'pin5red.gif',
16
+ p1: 'pin1.gif', p2: 'pin2.gif', p3: 'pin3.gif',
17
+ p4: 'pin4.gif', p5: 'pin5.gif', p6: 'pin6.gif',
18
+ p7: 'pin7.gif', p8: 'pin8.gif', p9: 'pin9.gif',
19
+
20
+ s0: 'sou5red.gif',
21
+ s1: 'sou1.gif', s2: 'sou2.gif', s3: 'sou3.gif',
22
+ s4: 'sou4.gif', s5: 'sou5.gif', s6: 'sou6.gif',
23
+ s7: 'sou7.gif', s8: 'sou8.gif', s9: 'sou9.gif',
24
+
25
+ z1: 'ton.gif', z2: 'nan.gif', z3: 'sha.gif', z4: 'pei.gif',
26
+ z5: 'haku.gif', z6: 'hatu.gif', z7: 'tyun.gif',
27
+
28
+ m0_: 'yman5red.gif',
29
+ m1_: 'yman1.gif', m2_: 'yman2.gif', m3_: 'yman3.gif',
30
+ m4_: 'yman4.gif', m5_: 'yman5.gif', m6_: 'yman6.gif',
31
+ m7_: 'yman7.gif', m8_: 'yman8.gif', m9_: 'yman9.gif',
32
+
33
+ p0_: 'ypin5red.gif',
34
+ p1_: 'ypin1.gif', p2_: 'ypin2.gif', p3_: 'ypin3.gif',
35
+ p4_: 'ypin4.gif', p5_: 'ypin5.gif', p6_: 'ypin6.gif',
36
+ p7_: 'ypin7.gif', p8_: 'ypin8.gif', p9_: 'ypin9.gif',
37
+
38
+ s0_: 'ysou5red.gif',
39
+ s1_: 'ysou1.gif', s2_: 'ysou2.gif', s3_: 'ysou3.gif',
40
+ s4_: 'ysou4.gif', s5_: 'ysou5.gif', s6_: 'ysou6.gif',
41
+ s7_: 'ysou7.gif', s8_: 'ysou8.gif', s9_: 'ysou9.gif',
42
+
43
+ z1_: 'yton.gif', z2_: 'ynan.gif', z3_: 'ysha.gif', z4_: 'ypei.gif',
44
+ z5_: 'yhaku.gif', z6_: 'yhatu.gif', z7_: 'ytyun.gif'
45
+ };
46
+
47
+ function markup(paistr, w, h) {
48
+
49
+ let url, v = 0;
50
+ let html = '<span class="l-mod-paiga" style="white-space:pre;">';
51
+
52
+ for (let pai of paistr.match(/[mpsz](?:\d+[\-\=]?)+|[ _]|.+/g)||[]) {
53
+
54
+ if (pai == ' ') {
55
+ html += ' ';
56
+ }
57
+ else if (pai == '_') {
58
+ url = imgbase + img._;
59
+ html += `<img src="${url}" width="${w}" height="${h}"`
60
+ + ` alt="${pai}">`;
61
+ }
62
+ else if (pai.match(/^[mpsz](?:\d+[\-\=]?)+/)) {
63
+ let s = pai[0];
64
+ for (let n of pai.match(/\d[\-\=]?/g)) {
65
+ let d = n[1]||''; n = n[0];
66
+ if (d == '=' && ! v) {
67
+ html += `<span style="display:inline-block;width:${h}px">`;
68
+ v = 1;
69
+ }
70
+ if (d || v) {
71
+ url = imgbase + img[s+n+'_'];
72
+ if (d == '=') {
73
+ html += `<img src="${url}" width="${h}" height="${w}"`
74
+ + ` style="vertical-align:bottom;display:block"`
75
+ + ` alt="${s+n+'='}">`;
76
+ }
77
+ else {
78
+ html += `<img src="${url}" width="${h}" height="${w}"`
79
+ + ` alt="${s+n+'-'}">`;
80
+ }
81
+ }
82
+ else {
83
+ url = imgbase + img[s+n];
84
+ html += `<img src="${url}" width="${w}" height="${h}"`
85
+ + ` alt="${s+n}">`;
86
+ }
87
+ if (d != '=') {
88
+ if (v) html += '</span>';
89
+ v = 0;
90
+ }
91
+ }
92
+ }
93
+ else {
94
+ html += `<span style="color:red;">${pai}</span>`;
95
+ }
96
+ }
97
+ if (v) html += '</span>';
98
+ html += '</span>';
99
+ return html;
100
+ }
101
+
102
+ module.exports = class Paiga {
103
+
104
+ constructor(parser) {
105
+ this._parser = parser;
106
+ this._r = parser._r;
107
+ this._req = parser._r._req;
108
+ this._inline = ['paiga'];
109
+ this._block = [];
110
+ this._np = [];
111
+ }
112
+
113
+ paiga(type, param, value) {
114
+ let w = 24, h = 34;
115
+ if (! param) { w = 24; h = 34 }
116
+ else if (param == 'L') { w = 24; h = 34 }
117
+ else if (param == 'M') { w = 19; h = 24 }
118
+ else if (param == 'S') { w = 16; h = 23 }
119
+ else if (param.match(/^\d+x\d+$/)) {
120
+ [ w, h ] = param.split(/x/);
121
+ }
122
+ return markup(value, w, h);
123
+ }
124
+ }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * module/pinyin
3
+ */
4
+ "use strict";
5
+
6
+ const tone_letter = {
7
+ a: ['ā','á','ǎ','à'],
8
+ e: ['ē','é','ě','è'],
9
+ o: ['ō','ó','ǒ','ò'],
10
+ i: ['ī','í','ǐ','ì'],
11
+ u: ['ū','ú','ǔ','ù'],
12
+ v: ['ǖ','ǘ','ǚ','ǜ'],
13
+ n: ['n̄','ń','ň','ǹ'],
14
+ A: ['Ā','Á','Ǎ','À'],
15
+ E: ['Ē','É','Ě','È'],
16
+ O: ['Ō','Ó','Ǒ','Ò'],
17
+ I: ['Ī','Í','Ǐ','Ì'],
18
+ U: ['Ū','Ú','Ǔ','Ù'],
19
+ V: ['Ǖ','Ǘ','Ǚ','Ǜ'],
20
+ N: ['N̄','Ń','Ň','Ǹ'],
21
+ };
22
+
23
+ function mark(str) {
24
+ const code = (c)=>tone_letter[c][n-1];
25
+ let [ , s, n ] = str.match(/^(.*?)(\d)$/);
26
+ if (s.match(/[aeo]/i)) return s.replace(/[aeo]/i, code);
27
+ if (s.match(/[iu]$/i)) return s.replace(/[iu]$/i, code);
28
+ if (s.match(/^[iuv]/i)) return s.replace(/^[iuv]/i, code);
29
+ else return s.replace(/n/i, code);
30
+ }
31
+
32
+ module.exports = class Pinyin {
33
+
34
+ constructor(parser) {
35
+ this._parser = parser;
36
+ this._r = parser._r;
37
+ this._req = parser._r._req;
38
+ this._inline = ['pinyin'];
39
+ this._block = ['pinyin'];
40
+ this._np = [];
41
+ }
42
+
43
+ pinyin(type, param, value) {
44
+ value = value.replace(/([aeouiv][1234])([aeo])/ig, '$1-$2');
45
+ value = value.replace(/[iuv]?[aeo]?(?:i|u|o|n|ng)?[1234]/ig, mark);
46
+ value = value.replace(/v/g, 'ü').replace(/V/g, 'Ü');
47
+ return type == '#'
48
+ ? `<div class="l-mod-pinyin">${value}</div>`
49
+ : `<span class="l-mod-pinyin">${value}</span>`;
50
+ }
51
+ }
@@ -7,6 +7,7 @@ const fs = require('fs').promises;
7
7
  const { join } = require('path');
8
8
  const HTML = require('../html/folder');
9
9
  const parse = require('../text/liulian');
10
+ const md = require('../text/markdown');
10
11
 
11
12
  const File = require('./file');
12
13
 
@@ -39,7 +40,7 @@ module.exports = class Folder extends File {
39
40
  catch(err) {}
40
41
  }
41
42
  if (this._req.cmd != 'edit') {
42
- for (let ext of ['', '.html', '.htm']) {
43
+ for (let ext of ['', '.md', '.html', '.htm']) {
43
44
  let index = this._files.find(f=>f.name == 'index' + ext);
44
45
  if (index) return index.open();
45
46
  }
@@ -99,11 +100,19 @@ module.exports = class Folder extends File {
99
100
  res.sendText(new HTML(this).edit());
100
101
  }
101
102
  else {
102
- let readme = this._files.find(f=>f.name == 'README');
103
+ let readme;
104
+ for (let file of ['README','README.md']) {
105
+ readme = this._files.find(f=>f.name == file);
106
+ if (readme) break;
107
+ }
103
108
  if (readme) {
104
109
  await readme.open();
105
110
  readme._html = new HTML(this);
106
- res.sendText(readme._html.folder(await parse(readme)));
111
+ res.sendText(readme._html.folder(
112
+ readme.type == 'text/x-liulian'
113
+ ? await parse(readme)
114
+ : md.render(readme._text)
115
+ ));
107
116
  }
108
117
  else if (this._req.user) {
109
118
  res.sendText(new HTML(this).folder());
@@ -11,6 +11,7 @@ const File = require('./file');
11
11
  const Folder = require('./folder');
12
12
  const Text = require('./text');
13
13
  const LiuLian = require('./liulian');
14
+ const Markdown = require('./markdown');
14
15
 
15
16
  async function resource(req, file) {
16
17
  let location;
@@ -35,6 +36,12 @@ async function resource(req, file) {
35
36
  else if (! basename(path).match(/\./))
36
37
  return new LiuLian(req, path, stat,
37
38
  location, resource);
39
+ else if (mime.getType(path) == 'text/markdown')
40
+ return new Markdown (req, path, stat,
41
+ location, resource);
42
+ else if (mime.getType(path) == 'application/javascript')
43
+ return new Text (req, path, stat,
44
+ location, resource);
38
45
  else if ((mime.getType(path)||'').match(/^text\//))
39
46
  return new Text(req, path, stat,
40
47
  location, resource);
@@ -18,44 +18,18 @@ module.exports = class LiuLian extends Text {
18
18
  script(script) { this._html.script(script) }
19
19
  meta(attr) { this._html.meta(attr) }
20
20
 
21
- async _seekToParent(filename) {
22
- let pathDir = this._req.pathDir;
23
- while (pathDir) {
24
- try {
25
- const r = await this.openFile(this._req, pathDir + filename);
26
- await r.open();
27
- return r.text;
28
- }
29
- catch(e) {
30
- pathDir = pathDir.replace(/[^\/]*\/$/,'');
31
- }
32
- }
33
- return '';
34
- }
35
-
36
21
  async update() {
37
22
  await super.update();
38
23
  if (this._req.param('text')) this._redirect = [303, this.name];
39
24
  }
40
25
 
41
- async send(res) {
42
- if (this._req.cmd == 'edit') {
43
- res.sendText(new HTML(this).edit());
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
- }
51
- else {
52
- this._text = (this.name != 'HEAD'
53
- ? await this._seekToParent('HEAD') + '\n' : '')
54
- + this._text + '\n'
55
- + (this.name != 'TAIL'
56
- ? await this._seekToParent('TAIL') : '');
57
- this._html = new HTML(this);
58
- res.sendText(this._html.stringify(await parse(this)));
59
- }
26
+ async _send(res) {
27
+ this._text = (this.name != 'HEAD'
28
+ ? await this._seekToParent('HEAD') + '\n' : '')
29
+ + this._text + '\n'
30
+ + (this.name != 'TAIL'
31
+ ? await this._seekToParent('TAIL') : '');
32
+ this._html = new HTML(this);
33
+ res.sendText(this._html.stringify(await parse(this)));
60
34
  }
61
35
  }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * resource/markdown
3
+ */
4
+ "use strict";
5
+
6
+ const { strip } = require('../util/html-escape');
7
+
8
+ const HTML = require('../html/text');
9
+ const Text = require('./text');
10
+ const md = require('../text/markdown');
11
+
12
+ module.exports = class Markdown extends Text {
13
+
14
+ async update() {
15
+ await super.update();
16
+ if (this._req.param('text')) this._redirect = [303, this.name];
17
+ }
18
+
19
+ async _send(res) {
20
+ this._text = (this.name != 'HEAD.md'
21
+ ? await this._seekToParent('HEAD.md') + '\n' : '')
22
+ + this._text + '\n'
23
+ + (this.name != 'TAIL.md'
24
+ ? await this._seekToParent('TAIL.md') : '');
25
+ let html = md.render(this._text);
26
+ let title = strip((html.match(/<title>(.*?)<\/title>/i)||
27
+ html.match(/<h1>(.*?)<\/h1>/i)||[])[1]);
28
+ let style = (html.match(/<link\s?.*?>/ig)||[])
29
+ .filter(tag=>tag.match(/\srel="stylesheet"/i));
30
+ this._html = new HTML(this);
31
+ res.sendText(this._html.title(title).stringify(html, ! style.length));
32
+ }
33
+ }
@@ -13,6 +13,21 @@ module.exports = class Text extends File {
13
13
  get text() { return this._text }
14
14
  get diff() { return this._diff }
15
15
 
16
+ async _seekToParent(filename) {
17
+ let pathDir = this._req.pathDir;
18
+ while (pathDir) {
19
+ try {
20
+ const r = await this.openFile(this._req, pathDir + filename);
21
+ await r.open();
22
+ return r.text;
23
+ }
24
+ catch(e) {
25
+ pathDir = pathDir.replace(/[^\/]*\/$/,'');
26
+ }
27
+ }
28
+ return '';
29
+ }
30
+
16
31
  async open() {
17
32
  await super.open();
18
33
  if (this._backup && this._req.cmd == 'diff') {
@@ -57,6 +72,10 @@ module.exports = class Text extends File {
57
72
  if (this._req.cmd == 'edit') res.sendText(new HTML(this).edit());
58
73
  else if (this._req.cmd == 'log') res.sendText(new HTML(this).log());
59
74
  else if (this._req.cmd == 'diff') res.sendText(new HTML(this).diff());
60
- else res.sendFile(this._path, this.type);
75
+ else this._send(res);
76
+ }
77
+
78
+ _send(res) {
79
+ res.sendFile(this._path, this.type);
61
80
  }
62
81
  }
@@ -381,6 +381,7 @@ class LiuLian {
381
381
  let [ , name, param, eod ]
382
382
  = line.match(/^#(\w+)(?:\((.*?)\))?(?:<<(\S+))?$/);
383
383
  if (name == '_') return await this.tag(line, param, eod);
384
+ if (name == 'import') return this.import(line, param);
384
385
 
385
386
  return await this._module.callBlockModule(line, name, param, eod);
386
387
  }
@@ -402,6 +403,11 @@ class LiuLian {
402
403
  }
403
404
  }
404
405
 
406
+ import(line, param) {
407
+ if (this._module.import(param)) return '';
408
+ return '<div style="color:red">' + cdata(line) + '</div>\n\n';
409
+ }
410
+
405
411
  inlineModule(str, name, param, value) {
406
412
  return this._module.callInlineModule(str, name, param, value);
407
413
  }
@@ -0,0 +1,25 @@
1
+ /*
2
+ * text/markdown
3
+ */
4
+ "use strict";
5
+
6
+ const hljs = require('highlight.js');
7
+
8
+ module.exports = require('markdown-it')({
9
+ html: true,
10
+ linkify: true,
11
+ highlight: function(str, lang) {
12
+ const error = console.error; console.error = ()=>{};
13
+ let html = '';
14
+ if (lang) {
15
+ try {
16
+ html = hljs.highlight(lang, str).value;
17
+ }
18
+ catch(err) {
19
+ html = hljs.highlightAuto(str).value;
20
+ }
21
+ }
22
+ console.error = error;
23
+ return html;
24
+ }
25
+ }).use(require('markdown-it-footnote'));
@@ -15,8 +15,13 @@ function cdata(str = '') {
15
15
 
16
16
  const cref = replacer(cref_pt, null, cdata);
17
17
 
18
+ function getAlt(img) {
19
+ return cref((img.match(/\salt="(.*?)"/)||[])[1]);
20
+ }
21
+
18
22
  function strip(html = '') {
19
- return html.replace(/<.*?>/g, '')
23
+ return html.replace(/<img\s?.*?>/g, getAlt)
24
+ .replace(/<.*?>/g, '')
20
25
  .replace(/&gt;/g, '>')
21
26
  .replace(/&lt;/g, '<')
22
27
  .replace(/&quot;/g,'"')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kobalab/liulian",
3
- "version": "0.8.1",
3
+ "version": "1.0.0",
4
4
  "description": "Node.jsで動作するWebサイト作成ツール",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -34,6 +34,8 @@
34
34
  "express": "^4.17.1",
35
35
  "express-session": "^1.17.1",
36
36
  "highlight.js": "^10.4.1",
37
+ "markdown-it": "^12.3.2",
38
+ "markdown-it-footnote": "^3.0.3",
37
39
  "mime": "^2.4.6",
38
40
  "multer": "^1.4.2",
39
41
  "passport": "^0.4.1",