├── .gitignore
├── .npmignore
├── test
├── files
│ ├── text.md
│ ├── list.md
│ ├── box.md
│ └── table.md
├── clean.js
├── style.js
└── parse.js
├── src
├── lib
│ ├── box.js
│ ├── rules.js
│ ├── styles.js
│ ├── polyfill.js
│ ├── utils.js
│ ├── parse.js
│ └── render.js
└── index.js
├── examples
├── index.js
└── complex.md
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *log
3 | dev
4 | .DS_Store
5 | .idea
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *log
3 | dev
4 | .DS_Store
5 | .idea
6 | test
7 | examples
--------------------------------------------------------------------------------
/test/files/text.md:
--------------------------------------------------------------------------------
1 | [color=redBright][b][bgGreen]No support[/bgGreen][/b][/color]
2 | No support
--------------------------------------------------------------------------------
/src/lib/box.js:
--------------------------------------------------------------------------------
1 | exports.single = {
2 | topLeft: '┌',
3 | topRight: '┐',
4 | bottomRight: '┘',
5 | bottomLeft: '└',
6 | vertical: '│',
7 | horizontal: '─'
8 | };
9 |
10 | exports.double = {
11 | topLeft: '╔',
12 | topRight: '╗',
13 | bottomRight: '╝',
14 | bottomLeft: '╚',
15 | vertical: '║',
16 | horizontal: '═'
17 | };
--------------------------------------------------------------------------------
/examples/index.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var Cubb = require('../src');
4 |
5 | var options = {
6 | box: {
7 | paddingHorizontal: 3,
8 | paddingVertical: 1
9 | },
10 | list: {
11 | space: 2,
12 | style: '+'
13 | }
14 | };
15 |
16 | var cubb = new Cubb(options);
17 | var source = fs.readFileSync(path.resolve(__dirname, 'complex.md'), 'utf8');
18 |
19 | console.log(cubb.render(source));
--------------------------------------------------------------------------------
/test/clean.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var assert = require('assert');
4 | var render = require('../src/lib/render');
5 |
6 | var source = fs.readFileSync(path.resolve(__dirname, 'files/text.md'), 'utf8');
7 | var texts = source.split(/\n/);
8 |
9 | describe('Clean', function() {
10 | it('should remove all tags -- ubb', function() {
11 | assert.equal('No support', render.clean(texts[0]));
12 | });
13 |
14 | it('should remove all tags -- html', function() {
15 | assert.equal('No support', render.clean(texts[1]));
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/lib/rules.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils');
2 |
3 | var rules = {
4 | box: /^(:?)[-=]{5,}(:?)\n((?:[|║](?:.+)[|║]\n?)+)\n(?::?)[-=]{5,}(?::?)/,
5 | list: /^( *bullet) [\s\S]+?(?:\n(?!\1)\n?|\s*$)/,
6 | table: /^(?:(?: *\| *(.+) *\| *\n)(?: *\| *([-:]+[-| :]*) *\| *\n))?((?: *\|.*\| *(?:\n|$))+)/,
7 | text: /^[^\n]+/,
8 | newline: /^\n+/
9 | };
10 |
11 | rules.bullet = /(?:[*+])/;
12 | rules.item = /^( *bullet) [^\n]*/;
13 |
14 | rules.list = utils.replace(rules.list)(/bullet/g, rules.bullet)();
15 | rules.item = utils.replace(rules.item, 'gm')(/bullet/g, rules.bullet)();
16 |
17 | module.exports = rules;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cubb",
3 | "version": "1.0.5",
4 | "description": "cubb是一种命令行的文本标记语言, 用于输出更友好的命令行文本。",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "test": "./node_modules/.bin/mocha",
8 | "example": "node ./examples/index"
9 | },
10 | "keywords": [
11 | "command",
12 | "cli",
13 | "ubb",
14 | "chalk"
15 | ],
16 | "author": "Spikef",
17 | "license": "MIT",
18 | "repository": {
19 | "type": "git",
20 | "url": "git+ssh://@github.com:Spikef/cubb.git"
21 | },
22 | "dependencies": {},
23 | "devDependencies": {
24 | "chalk": "^2.0.1",
25 | "mocha": "^10.2.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/test/style.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var assert = require('assert');
4 | var chalk = require('chalk');
5 | var render = require('../src/lib/render');
6 |
7 | var source = fs.readFileSync(path.resolve(__dirname, 'files/text.md'), 'utf8');
8 | var texts = source.split(/\n/);
9 |
10 | describe('Style', function() {
11 | it('should return a stylish text -- ubb', function() {
12 | assert.equal(chalk.redBright.bold.bgGreen('No support'), render.style(texts[0]));
13 | });
14 |
15 | it('should return a stylish text -- html', function() {
16 | assert.equal(chalk.redBright.bold.bgGreen('No support'), render.style(texts[1]));
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/test/files/list.md:
--------------------------------------------------------------------------------
1 | + list item1
2 | + list item2
3 | + list item3
4 | + list item4
5 | + list item5
6 | + list item6
7 |
8 | * list item7
9 | * list item8
10 | * list item9
11 |
12 |
13 | * list item10
14 | * list item11
15 |
16 | --------------
17 |
18 | [
19 | {
20 | "type": "list",
21 | "items": [
22 | "list item1",
23 | "list item2",
24 | "list item3"
25 | ]
26 | },
27 | {
28 | "type": "list",
29 | "items": [
30 | "list item4",
31 | "list item5",
32 | "list item6"
33 | ]
34 | },
35 | {
36 | "type": "list",
37 | "items": [
38 | "list item7",
39 | "list item8",
40 | "list item9"
41 | ]
42 | },
43 | {
44 | "type": "space",
45 | "value": "\n"
46 | },
47 | {
48 | "type": "list",
49 | "items": [
50 | "list item10",
51 | "list item11"
52 | ]
53 | }
54 | ]
--------------------------------------------------------------------------------
/test/files/box.md:
--------------------------------------------------------------------------------
1 | =================:
2 | ║ some text ║
3 | ║ some long text ║
4 | =================:
5 |
6 | :----------------:
7 | | some text |
8 | | some long text |
9 | :----------------:
10 |
11 |
12 |
13 |
14 | [
15 | {
16 | "type": "box",
17 | "width": 14,
18 | "align": "right",
19 | "border": "double",
20 | "rows": [
21 | {
22 | "value": "some text",
23 | "width": 9
24 | },
25 | {
26 | "value": "some long text",
27 | "width": 14
28 | }
29 | ]
30 | },
31 | {
32 | "type": "space",
33 | "value": "\n\n"
34 | },
35 | {
36 | "type": "box",
37 | "width": 14,
38 | "align": "center",
39 | "border": "single",
40 | "rows": [
41 | {
42 | "value": "some text",
43 | "width": 9
44 | },
45 | {
46 | "value": "some long text",
47 | "width": 14
48 | }
49 | ]
50 | }
51 | ]
--------------------------------------------------------------------------------
/test/parse.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var assert = require('assert');
4 | var chalk = require('chalk');
5 | var parse = require('../src/lib/parse');
6 |
7 | var source;
8 |
9 | source = fs.readFileSync(path.resolve(__dirname, 'files/box.md'), 'utf8');
10 | var boxes = source.split(/\n{3,}\n/);
11 |
12 | source = fs.readFileSync(path.resolve(__dirname, 'files/list.md'), 'utf8');
13 | var lists = source.split(/\n-{3,}\n/);
14 |
15 | source = fs.readFileSync(path.resolve(__dirname, 'files/table.md'), 'utf8');
16 | var tables = source.split(/\n-{3,}\n/);
17 |
18 | describe('Parse', function() {
19 | it('parse box', function() {
20 | var tokens = parse(boxes[0], { tab: ' ' });
21 | assert.equal(JSON.stringify(tokens), JSON.stringify(JSON.parse(boxes[1].replace(/^\s+|\s+$/g, ''))));
22 | });
23 |
24 | it('parse list', function() {
25 | var tokens = parse(lists[0], { tab: ' ' });
26 | assert.equal(JSON.stringify(tokens), JSON.stringify(JSON.parse(lists[1].replace(/^\s+|\s+$/g, ''))));
27 | });
28 |
29 | it('parse table with header', function() {
30 | var tokens = parse(tables[0], { tab: ' ' });
31 | assert.equal(JSON.stringify(tokens), JSON.stringify(JSON.parse(tables[2].replace(/^\s+|\s+$/g, ''))));
32 | });
33 | });
--------------------------------------------------------------------------------
/src/lib/styles.js:
--------------------------------------------------------------------------------
1 | var styles = {
2 | bold: [1, 22],
3 | italic: [3, 23],
4 | diminish: [2, 22],
5 | underline: [4, 24],
6 | // color
7 | black: 30,
8 | red: 31,
9 | green: 32,
10 | yellow: 33,
11 | blue: 34,
12 | magenta: 35,
13 | cyan: 36,
14 | white: 37,
15 | gray: 90,
16 | // Bright color
17 | redBright: 91,
18 | greenBright: 92,
19 | yellowBright: 93,
20 | blueBright: 94,
21 | magentaBright: 95,
22 | cyanBright: 96,
23 | whiteBright: 97,
24 | // close color
25 | colorClose: 39,
26 | // bgColor
27 | bgBlack: 40,
28 | bgRed: 41,
29 | bgGreen: 42,
30 | bgYellow: 43,
31 | bgBlue: 44,
32 | bgMagenta: 45,
33 | bgCyan: 46,
34 | bgWhite: 47,
35 | // Bright bg color
36 | bgBlackBright: 100,
37 | bgRedBright: 101,
38 | bgGreenBright: 102,
39 | bgYellowBright: 103,
40 | bgBlueBright: 104,
41 | bgMagentaBright: 105,
42 | bgCyanBright: 106,
43 | bgWhiteBright: 107,
44 | // close bgColor
45 | bgColorClose: 49
46 | };
47 |
48 | Object.keys(styles).forEach(function(key) {
49 | if (Array.isArray(styles[key])) {
50 | styles[key] = styles[key].map(function(code) {
51 | return '\u001B[' + code + 'm';
52 | });
53 | } else {
54 | styles[key] = '\u001B[' + styles[key] + 'm';
55 | }
56 | });
57 |
58 | module.exports = styles;
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | require('./lib/polyfill');
2 |
3 | var utils = require('./lib/utils');
4 | var parse = require('./lib/parse');
5 | var render = require('./lib/render');
6 |
7 | var configs = {
8 | tab: ' ',
9 | width: 'auto',
10 | box: {
11 | width: 'auto',
12 | margin: 1,
13 | padding: 2,
14 | borderColor: 'yellow',
15 | paddingVertical: 0,
16 | paddingHorizontal: 1
17 | },
18 | list: {
19 | width: 'auto',
20 | style: '*',
21 | space: 0,
22 | margin: 1,
23 | padding: 2
24 | },
25 | table: {
26 | width: 'auto',
27 | margin: 1,
28 | padding: 2,
29 | rowSpace: 0,
30 | colSpace: 4,
31 | titlePadding: 2
32 | }
33 | };
34 |
35 | function Cubb(options) {
36 | if (!this instanceof Cubb) {
37 | return new Cubb(options);
38 | }
39 |
40 | this.options = utils.merge(configs, options);
41 | }
42 |
43 | /**
44 | * render string
45 | * @param {string} string
46 | * @param {Object} [options] - options
47 | * @param {string} [options.tab=' '] - \t
48 | * @param {number} [options.width='auto'] - max width
49 | */
50 | Cubb.prototype.render = function(string, options) {
51 | if (typeof string !== 'string') {
52 | throw new Error('String is needed!');
53 | }
54 |
55 | var opts;
56 | if (options) {
57 | opts = utils.merge(this.options, options);
58 | } else {
59 | opts = this.options;
60 | }
61 |
62 | var tokens = parse(string, opts);
63 | return render(tokens, opts);
64 | };
65 |
66 | Cubb.prototype.clean = render.clean;
67 | Cubb.clean = render.clean;
68 |
69 | module.exports = Cubb;
--------------------------------------------------------------------------------
/src/lib/polyfill.js:
--------------------------------------------------------------------------------
1 | /*! http://mths.be/codepointat v0.1.0 by @mathias */
2 | if (!String.prototype.codePointAt) {
3 | (function() {
4 | 'use strict'; // 严格模式,needed to support `apply`/`call` with `undefined`/`null`
5 | var codePointAt = function(position) {
6 | if (this == null) {
7 | throw TypeError();
8 | }
9 | var string = String(this);
10 | var size = string.length;
11 | // 变成整数
12 | var index = position ? Number(position) : 0;
13 | if (index != index) { // better `isNaN`
14 | index = 0;
15 | }
16 | // 边界
17 | if (index < 0 || index >= size) {
18 | return undefined;
19 | }
20 | // 第一个编码单元
21 | var first = string.charCodeAt(index);
22 | var second;
23 | if ( // 检查是否开始 surrogate pair
24 | first >= 0xD800 && first <= 0xDBFF && // high surrogate
25 | size > index + 1 // 下一个编码单元
26 | ) {
27 | second = string.charCodeAt(index + 1);
28 | if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
29 | // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
30 | return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
31 | }
32 | }
33 | return first;
34 | };
35 | if (Object.defineProperty) {
36 | Object.defineProperty(String.prototype, 'codePointAt', {
37 | 'value': codePointAt,
38 | 'configurable': true,
39 | 'writable': true
40 | });
41 | } else {
42 | String.prototype.codePointAt = codePointAt;
43 | }
44 | }());
45 | }
--------------------------------------------------------------------------------
/examples/complex.md:
--------------------------------------------------------------------------------
1 | The new APIs in ES6 come in three flavours: Symbol, Reflect, and Proxy. Upon first glance this might be a little confusing - three separate APIs all for metaprogramming? But it actually makes a lot of sense when you see how each one is split:
2 | * Symbols are all about Reflection within implementation - you sprinkle them on your existing classes and objects to change the behaviour.
3 | * Reflect is all about Reflection through introspection - used to discover very low level information about your code.
4 | * Proxy is all about Reflection through intercession - wrapping objects and intercepting their behaviours through traps.
5 |
6 | | Feature | Chrome | Edge | Firefox (Gecko) | Internet Explorer | Opera | Safari |
7 | | ----------------------------------- | :------: | :----------: | :--------------: | :--------------------------: | :----: | :----: |
8 | | [title 1] |
9 | | Basic support | 38 | 12[1] | 36 (36) | No support | 25 | 9 |
10 | | Symbol.iterator (@@iterator) | 38 | 12 | 36 (36) | No support | 25 | 9 |
11 | | [title 2] |
12 | | Symbol.unscopables (@@unscopables) | 38 | 12 | 48 (48) | No support | 25 | 9 |
13 | | Symbol.species (@@species) | 51 | ?[2] | 41 (41) | No support | ? | ? |
14 |
15 | -----------------------------------------------------------------------------
16 | | [1] Edge 12 included Symbol properties in JSON.stringify() output. |
17 | | |
18 | | [2] No data about this. |
19 | -----------------------------------------------------------------------------
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | exports.capitalize = function capitalize(word) {
2 | if (typeof word !== 'string') {
3 | throw new Error('Argument type error: ' + word);
4 | }
5 |
6 | return word.toLowerCase().replace(/^[a-z]/, function($0) {
7 | return $0.toUpperCase();
8 | });
9 | };
10 |
11 | exports.merge = function merge() {
12 | var target = {};
13 |
14 | for (var index = 0; index < arguments.length; index++) {
15 | var source = arguments[index];
16 | if (source != null) {
17 | for (var key in source) {
18 | if (Object.prototype.hasOwnProperty.call(source, key)) {
19 | if (source[key] && typeof source[key] === 'object') {
20 | target[key] = merge(target[key], source[key]);
21 | } else {
22 | target[key] = source[key];
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
29 | return target;
30 | };
31 |
32 | exports.trim = function(str) {
33 | return str.replace(/^\s+|\s+$/g, '');
34 | };
35 |
36 | exports.replace = function replace(regex, opt) {
37 | regex = regex.source;
38 | opt = opt || '';
39 |
40 | return function self(name, val) {
41 | if (!name) return new RegExp(regex, opt);
42 | val = val.source || val;
43 | val = val.replace(/(^|[^\[])\^/g, '$1');
44 | regex = regex.replace(name, val);
45 | return self;
46 | };
47 | };
48 |
49 | exports.repeatChars = function repeatChars(char, count) {
50 | if (arguments.length <= 1) {
51 | count = char;
52 | char = ' ';
53 | }
54 |
55 | return new Array(parseInt(count) + 1).join(char);
56 | };
57 |
58 | exports.stringWidth = function stringWidth(str) {
59 | if (typeof str !== 'string' || str.length === 0) {
60 | return 0;
61 | }
62 |
63 | var width = 0;
64 |
65 | for (var i = 0; i < str.length; i++) {
66 | var code = str.codePointAt(i);
67 |
68 | // Ignore control characters
69 | if (code <= 0x1F || (code >= 0x7F && code <= 0x9F)) {
70 | continue;
71 | }
72 |
73 | // Ignore combining characters
74 | if (code >= 0x300 && code <= 0x36F) {
75 | continue;
76 | }
77 |
78 | // Surrogates
79 | if (code > 0xFFFF) {
80 | i++;
81 | }
82 |
83 | width += this.isWideChar(code) ? 2 : 1;
84 | }
85 |
86 | return width;
87 | };
88 |
89 | exports.isWideChar = function isWideChar(code) {
90 | if (isNaN(code)) return false;
91 |
92 | // code points are derived from:
93 | // http://www.unix.org/Public/UNIDATA/EastAsianWidth.txt
94 | return code >= 0x1100 && (
95 | code <= 0x115f || // Hangul Jamo
96 | code === 0x2329 || // LEFT-POINTING ANGLE BRACKET
97 | code === 0x232a || // RIGHT-POINTING ANGLE BRACKET
98 | // CJK Radicals Supplement .. Enclosed CJK Letters and Months
99 | (0x2e80 <= code && code <= 0x3247 && code !== 0x303f) ||
100 | // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
101 | (0x3250 <= code && code <= 0x4dbf) ||
102 | // CJK Unified Ideographs .. Yi Radicals
103 | (0x4e00 <= code && code <= 0xa4c6) ||
104 | // Hangul Jamo Extended-A
105 | (0xa960 <= code && code <= 0xa97c) ||
106 | // Hangul Syllables
107 | (0xac00 <= code && code <= 0xd7a3) ||
108 | // CJK Compatibility Ideographs
109 | (0xf900 <= code && code <= 0xfaff) ||
110 | // Vertical Forms
111 | (0xfe10 <= code && code <= 0xfe19) ||
112 | // CJK Compatibility Forms .. Small Form Variants
113 | (0xfe30 <= code && code <= 0xfe6b) ||
114 | // Halfwidth and Fullwidth Forms
115 | (0xff01 <= code && code <= 0xff60) ||
116 | (0xffe0 <= code && code <= 0xffe6) ||
117 | // Kana Supplement
118 | (0x1b000 <= code && code <= 0x1b001) ||
119 | // Enclosed Ideographic Supplement
120 | (0x1f200 <= code && code <= 0x1f251) ||
121 | // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane
122 | (0x20000 <= code && code <= 0x3fffd)
123 | );
124 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cubb
2 |
3 | `cubb`是一种命令行的文本标记语言,用于输出更友好的命令行文本。
4 |
5 | 
6 |
7 | ## 安装与使用
8 |
9 | ### 安装
10 |
11 | ```bash
12 | $ npm install cubb --save
13 | ```
14 |
15 | ### 使用
16 |
17 | ```javascript
18 | var options = {};
19 | var Cubb = require('cubb');
20 | var cubb = new Cubb(options);
21 | cubb.render('some text');
22 | ```
23 |
24 | ## 选项
25 |
26 | 构造函数支持传入渲染选项
27 |
28 | * options.tab: 如果显示`\t`,默认为4空格
29 | * options.box: 盒子渲染选项
30 | * options.box.margin: 盒子与前后内容之间的行距,默认为1行
31 | * options.box.padding: 盒子左内补,默认为2空格
32 | * options.box.borderColor: 边框颜色,默认为'yellow'
33 | * options.box.paddingVertical: 盒子内垂直间距,默认为0行
34 | * options.box.paddingHorizontal: 盒子内水平间距,默认为1空格
35 | * options.list: 列表渲染选项
36 | * options.list.style: 列表项修饰符号
37 | * options.list.space: 列表项与列表项之间的行距,默认为0行
38 | * options.list.margin: 列表与前后内容之间的行距,默认为1行
39 | * options.list.padding: 列表左内补,默认为2空格
40 | * options.table: 表格渲染选项
41 | * options.table.margin: 表格与前后内容之间的行距,默认为1行
42 | * options.table.padding: 表格左内补,默认为2空格
43 | * options.table.rowSpace: 两行之间的行距,默认为0行
44 | * options.table.colSpace: 两个单元格之间的间距,默认为4空格
45 | * options.table.titlePadding: 标题行左内补,默认为2空格
46 |
47 | ## 方法
48 |
49 | ### render(text,options)
50 |
51 | 将标记文本渲染成可以在命令行正确显示的文本。
52 |
53 | * text: 要渲染的完整文本内容
54 | * options: 同选项,只对当前渲染有效
55 |
56 | ### clean(text)
57 |
58 | 清除所有标记样式。
59 |
60 | ## 语法
61 |
62 | 所有语法标签都支持嵌套使用。
63 |
64 | ### 粗体
65 |
66 | ```html
67 | some text
68 | ```
69 |
70 | ```markdown
71 | [b]some text[/b]
72 | ```
73 |
74 | ### 斜体
75 |
76 | ```html
77 | some text
78 | ```
79 |
80 | ```markdown
81 | [b]some text[/b]
82 | ```
83 |
84 | ### 下划线
85 |
86 | ```html
87 | some text
88 | ```
89 |
90 | ```markdown
91 | [u]some text[/u]
92 | ```
93 |
94 | ### 弱化
95 |
96 | ```html
97 | some text
98 | ```
99 |
100 | ```markdown
101 | [d]some text[/d]
102 | ```
103 |
104 | ### 文本颜色
105 |
106 | 支持的颜色: black,red,green,yellow,blue,magenta,cyan,white,gray,redBright,greenBright,yellowBright,blueBright,magentaBright,cyanBright,whiteBright。
107 |
108 | ```html
109 | some text
110 | ```
111 |
112 | ```html
113 | some text
114 | ```
115 |
116 | ```markdown
117 | [red]some text[/red]
118 | ```
119 |
120 | ```markdown
121 | [color=red]some text[/color]
122 | ```
123 |
124 | ### 背景颜色
125 |
126 | ```html
127 | some text
128 | ```
129 |
130 | ```html
131 | some text
132 | ```
133 |
134 | ```markdown
135 | [bgRed]some text[/bgRed]
136 | ```
137 |
138 | ```markdown
139 | [bgColor=red]some text[/bgColor]
140 | ```
141 |
142 | ### 列表
143 |
144 | ```markdown
145 | * list item1
146 | * list item2
147 | * list item3
148 | ```
149 |
150 | ### 盒子
151 |
152 | #### 单边框
153 |
154 | ```markdown
155 | :----------------:
156 | | some text |
157 | | some long text |
158 | :----------------:
159 | ```
160 |
161 | #### 双边框
162 |
163 | ```markdown
164 | =================:
165 | ║ some text ║
166 | ║ some long text ║
167 | =================:
168 | ```
169 |
170 | #### 文本对齐
171 |
172 | 通过在左边或者右边添加`:`可以指定盒子内部文本的对齐方式。
173 |
174 | ### 表格
175 |
176 | #### 带表头表格
177 |
178 | 与markdown的表格一样,支持使用冒号定义单元格左右对齐方式。单元格的宽度是自动计算的,并不强行要求预先定义好。
179 |
180 | `title`行是一个特殊的行,需要使用`[]`将标题文本内容包裹起来。
181 |
182 | ```markdown
183 | | Feature | Chrome | Edge | Firefox (Gecko) | Internet Explorer | Opera | Safari |
184 | | ----------------------------------- | :------: | :----------: | :--------------: | :--------------------------: | :----: | :----: |
185 | | [title 1] |
186 | | Basic support | 38 | 12[1] | 36 (36) | No support | 25 | 9 |
187 | | Symbol.iterator (@@iterator) | 38 | 12 | 36 (36) | No support | 25 | 9 |
188 | | [title 2] |
189 | | Symbol.unscopables (@@unscopables) | 38 | 12 | 48 (48) | No support | 25 | 9 |
190 | | Symbol.species (@@species) | 51 | ? | 41 (41) | No support | ? | ? |
191 | ```
192 |
193 | #### 无表头表格
194 |
195 | 暂不支持定义单元格左右对齐方式。
196 |
197 | ```markdown
198 | | [title 1] |
199 | | Basic support | 38 | 12[1] | 36 (36) | No support | 25 | 9 |
200 | | Symbol.iterator (@@iterator) | 38 | 12 | 36 (36) | No support | 25 | 9 |
201 | | [title 2] |
202 | | Symbol.unscopables (@@unscopables) | 38 | 12 | 48 (48) | No support | 25 | 9 |
203 | | Symbol.species (@@species) | 51 | ? | 41 (41) | No support | ? | ? |
204 | ```
205 |
206 | ## 与chalk对比
207 |
208 | * `chalk`支持更多的颜色与样式定义
209 |
210 | `chalk`支持了一些只有在某些特定终端上才可以使用的特性,并在不兼容的时候做降级处理,`cubb`只实现了能够兼容所有终端的特性。
211 |
212 | * 使用方式不同
213 |
214 | 相比`chalk`采用api的方式,标记语言的方式使用起来更加直观方便。
215 |
216 | * `cubb`支持`list`和`table`块的渲染
217 |
218 | `chalk`只处理纯文本的渲染,相对而言,`cubb`更适合用于大段内容的渲染。
219 |
220 | ## 致谢
221 |
222 | 在实现`cubb`框架过程中,参考了`chalk`和`marked`的部分代码,在此表示感谢!
223 |
--------------------------------------------------------------------------------
/src/lib/parse.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils');
2 | var rules = require('./rules');
3 | var clean = require('./render').clean;
4 |
5 | function parse(string, options) {
6 | return token(serialize(string, options));
7 | }
8 |
9 | function serialize(string, options) {
10 | return string
11 | .replace(/\r(\n?)/g, '\n')
12 | .replace(/\t/g, options.tab)
13 | .replace(/\u00a0/g, ' ')
14 | .replace(/\u2424/g, '\n');
15 | }
16 |
17 | function token(str) {
18 | var tokens = [];
19 | var cap, block, bull, item, titles, aligns, header, i, l;
20 |
21 | while (str) {
22 | // box
23 | if (cap = rules.box.exec(str)) {
24 | str = str.substring(cap[0].length);
25 |
26 | block = {
27 | type: 'box',
28 | width: 0,
29 | align: null,
30 | border: /^:?=/.test(cap[0]) ? 'double' : 'single',
31 | rows: cap[3].replace(/^[|║]\s*|\s*[|║]$/g, '').split(/\s*[|║]\n[|║]\s*/)
32 | };
33 |
34 | if (cap[1] && cap[2]) {
35 | block.align = 'center';
36 | } else if (cap[1]) {
37 | block.align = 'left';
38 | } else if (cap[2]) {
39 | block.align = 'right';
40 | }
41 |
42 | block.rows = block.rows.map(function(value) {
43 | item = {
44 | value: value,
45 | width: utils.stringWidth(clean(value))
46 | };
47 |
48 | block.width = Math.max(block.width, item.width);
49 |
50 | return item;
51 | });
52 |
53 | tokens.push(block);
54 |
55 | continue;
56 | }
57 |
58 | // list
59 | if (cap = rules.list.exec(str)) {
60 | str = str.substring(cap[0].length);
61 | bull = cap[1];
62 |
63 | block = {
64 | type: 'list',
65 | items: []
66 | };
67 |
68 | // items
69 | cap = cap[0].match(rules.item);
70 |
71 | for (i = 0, l = cap.length; i < l; i++) {
72 | item = cap[i].replace(/^ *[*+] +/, '');
73 | block.items.push(item);
74 | }
75 |
76 | tokens.push(block);
77 |
78 | continue;
79 | }
80 |
81 | // table
82 | if (cap = rules.table.exec(str)) {
83 | str = str.substring(cap[0].length);
84 |
85 | block = {
86 | type: 'table',
87 | widths: []
88 | };
89 |
90 | if (cap[1] && cap[2]) {
91 | block.headers = [];
92 |
93 | titles = cap[1].split(/ +\| +/);
94 | aligns = cap[2].split(/ +\| +/);
95 |
96 | for (i = 0; i < titles.length; i++) {
97 | header = {
98 | title: titles[i].trim(),
99 | width: utils.stringWidth(clean(titles[i]))
100 | };
101 |
102 | block.widths[i] = block.widths[i] ? Math.max(header.width, block.widths[i]) : header.width;
103 |
104 | if (/^-+:$/.test(aligns[i])) {
105 | header.align = 'right';
106 | } else if (/^:-+:$/.test(aligns[i])) {
107 | header.align = 'center';
108 | } else if (/^:-+$/.test(aligns[i])) {
109 | header.align = 'left';
110 | } else {
111 | header.align = null;
112 | }
113 |
114 | block.headers.push(header);
115 | }
116 | }
117 |
118 | block.cells = cap[3].replace(/^\n+|\n+$/g, '').split('\n');
119 | block.cells = block.cells.map(function(cell, rowIndex) {
120 | cell = cell.replace(/^ *\||\| *$/g, '').split(/ +\| +/).map(utils.trim);
121 | item = {};
122 | item.rowIndex = rowIndex;
123 | item.type = cell.length === 1 && /^\[.*]$/.test(cell[0]) ? 'title' : 'row';
124 |
125 | if (item.type === 'title') {
126 | item.title = cell[0].replace(/^\[|]$/g, '');
127 | } else {
128 | item.cols = cell.map(function(col, colIndex) {
129 | col = {
130 | value: col,
131 | width: 0,
132 | colIndex: colIndex
133 | };
134 |
135 | col.width = utils.stringWidth(clean(col.value));
136 | col.align = block.headers && block.headers[colIndex] ? block.headers[colIndex].align : null;
137 |
138 | block.widths[colIndex] = block.widths[colIndex] ? Math.max(col.width, block.widths[colIndex]) : col.width;
139 |
140 | return col;
141 | });
142 | }
143 |
144 | return item;
145 | });
146 |
147 | tokens.push(block);
148 |
149 | continue;
150 | }
151 |
152 | // new line
153 | if (cap = rules.newline.exec(str)) {
154 | str = str.substring(cap[0].length);
155 |
156 | tokens.push({
157 | type: 'space',
158 | value: cap[0]
159 | });
160 |
161 | continue;
162 | }
163 |
164 | // text
165 | if (cap = rules.text.exec(str)) {
166 | str = str.substring(cap[0].length);
167 |
168 | tokens.push({
169 | type: 'text',
170 | value: cap[0]
171 | });
172 |
173 | continue;
174 | }
175 |
176 | if (str) {
177 | throw new Error('Infinite loop on byte: ' + str.charCodeAt(0));
178 | }
179 | }
180 |
181 | return tokens;
182 | }
183 |
184 | module.exports = parse;
--------------------------------------------------------------------------------
/src/lib/render.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils');
2 | var styles = require('./styles');
3 | var boxes = require('./box');
4 |
5 | var styleFlag = /[\[<]\/?(b|i|d|u|(bg)?color([=\s][^\]]+?)?|(bg)?(black|red|green|yellow|blue|magenta|cyan|white|gray)(bright)?)[\]>]/;
6 | var styleFlagStart = new RegExp('^' + styleFlag.source, 'i');
7 | var styleFlagGlobal = new RegExp(styleFlag.source, 'gi');
8 |
9 | function render(tokens, options) {
10 | var result = '';
11 |
12 | tokens.forEach(function(token) {
13 | switch (token.type) {
14 | case 'space':
15 | result += token.value;
16 | break;
17 | case 'text':
18 | result += renderText(token.value, options.width);
19 | break;
20 | case 'table':
21 | result += renderTable(token, options);
22 | break;
23 | case 'list':
24 | result += renderList(token, options);
25 | break;
26 | case 'box':
27 | result += renderBox(token, options);
28 | break;
29 | }
30 | });
31 |
32 | return result;
33 | }
34 |
35 | function renderText(str, width) {
36 | if (width === 'auto') return style(str);
37 |
38 | var cap, w = 0;
39 | var string = '';
40 |
41 | while (str) {
42 | if (w >= width) {
43 | if (cap = /^[ `~!&)=|}:;,\].>/?!…)—】;:”'。,、?]+/.exec(str)) {
44 | str = str.substring(cap[0].length);
45 | string += cap[0];
46 | }
47 |
48 | w = 0;
49 | string += '\n';
50 | continue;
51 | }
52 |
53 | if (cap = /^\n+/.exec(str)) {
54 | str = str.substring(cap[0].length);
55 | continue;
56 | }
57 |
58 | if (cap = styleFlagStart.exec(str)) {
59 | str = str.substring(cap[0].length);
60 | string += cap[0];
61 | continue;
62 | }
63 |
64 | if (cap = /^./.exec(str)) {
65 | str = str.substring(cap[0].length);
66 | string += cap[0];
67 | w += utils.stringWidth(cap[0]);
68 | continue;
69 | }
70 |
71 | if (str) {
72 | string += str;
73 | str = '';
74 | }
75 | }
76 |
77 | return style(string);
78 | }
79 |
80 | function renderBox(token, options) {
81 | var opts = options.box;
82 | var styles = boxes[token.border];
83 | var margin = utils.repeatChars('\n', opts.margin);
84 | var padding = utils.repeatChars(opts.padding);
85 | var paddingHorizontal = utils.repeatChars(opts.paddingHorizontal);
86 | var paddingVertical = utils.repeatChars('\n', opts.paddingVertical);
87 | var borderStyle = ['<' + opts.borderColor + '>', '' + opts.borderColor + '>'];
88 | var horizontal = utils.repeatChars(styles.horizontal, token.width + opts.paddingHorizontal * 2);
89 | var vertical = style(borderStyle[0] + styles.vertical + borderStyle[1]);
90 | var string = '';
91 |
92 | string += margin;
93 | string += padding + style(borderStyle[0] + styles.topLeft + horizontal + styles.topRight + borderStyle[1]);
94 | string += '\n';
95 |
96 | if (paddingVertical) {
97 | string += padding + vertical + utils.repeatChars(token.width + opts.paddingHorizontal * 2) + vertical;
98 | string += '\n';
99 | }
100 |
101 | string += token.rows.map(function(row) {
102 | row = renderCell(row.value, token.align, row.width, token.width);
103 | row = padding + vertical + paddingHorizontal + row + paddingHorizontal + vertical;
104 | return row;
105 | }).join('\n');
106 |
107 | if (paddingVertical) {
108 | string += '\n';
109 | string += padding + vertical + utils.repeatChars(token.width + opts.paddingHorizontal * 2) + vertical;
110 | }
111 |
112 | string += '\n';
113 | string += padding + style(borderStyle[0] + styles.bottomLeft + horizontal + styles.bottomRight + borderStyle[1]);
114 | string += margin;
115 |
116 | return string;
117 | }
118 |
119 | function renderList(token, options) {
120 | var opts = options.list;
121 | var space = utils.repeatChars('\n', opts.space);
122 | var margin = utils.repeatChars('\n', opts.margin);
123 | var bullet = utils.repeatChars(opts.padding) + opts.style + ' ';
124 |
125 | var string = token.items.map(function(item) {
126 | return bullet + style(item);
127 | }).join(space);
128 |
129 | string = margin + string + margin;
130 |
131 | return string;
132 | }
133 |
134 | function renderTable(token, options) {
135 | var opts = options.table;
136 | var margin = utils.repeatChars('\n', opts.margin);
137 | var padding = utils.repeatChars(opts.padding);
138 | var rowSpace = utils.repeatChars('\n', opts.rowSpace);
139 | var colSpace = utils.repeatChars(opts.colSpace);
140 | var titlePadding = utils.repeatChars(opts.titlePadding);
141 |
142 | var string = '';
143 |
144 | if (token.headers) {
145 | string += padding;
146 | token.headers.forEach(function(header, index) {
147 | if (index !== 0) string += colSpace;
148 | string += renderCell('' + header.title + '', header.align, header.width, token.widths[index]);
149 | });
150 | string += '\n';
151 |
152 | string += padding;
153 | token.widths.forEach(function(width, index) {
154 | if (index !== 0) string += colSpace;
155 | string += utils.repeatChars('=', width);
156 | });
157 | string += '\n';
158 |
159 | string += rowSpace;
160 | }
161 |
162 | token.cells.forEach(function(row, rowIndex) {
163 | if (rowIndex !== 0) string += rowSpace + '\n';
164 |
165 | if (row.type === 'title') {
166 | if (!rowSpace) string += '\n';
167 | string += titlePadding;
168 | string += style('' + row.title + '');
169 | if (!rowSpace) string += '\n';
170 | } else if (row.type === 'row') {
171 | string += padding;
172 | row.cols.forEach(function(col, colIndex) {
173 | if (colIndex !== 0) string += colSpace;
174 | string += renderCell(col.value, col.align, col.width, token.widths[colIndex]);
175 | });
176 | }
177 | });
178 |
179 | string = margin + string + margin;
180 |
181 | return string;
182 | }
183 |
184 | function renderCell(text, align, textWidth, cellWidth) {
185 | var spaceWidth = cellWidth - textWidth;
186 | var leftWidth, rightWidth, leftSpace, rightSpace, string;
187 | switch (align) {
188 | case 'center':
189 | leftWidth = Math.ceil(spaceWidth / 2);
190 | rightWidth = spaceWidth - leftWidth;
191 | leftSpace = utils.repeatChars(leftWidth);
192 | rightSpace = utils.repeatChars(rightWidth);
193 | string = leftSpace + text + rightSpace;
194 | break;
195 | case 'right':
196 | leftSpace = utils.repeatChars(spaceWidth);
197 | string = leftSpace + text;
198 | break;
199 | case 'left':
200 | case null:
201 | default:
202 | rightSpace = utils.repeatChars(spaceWidth);
203 | string = text + rightSpace;
204 | break;
205 | }
206 |
207 | return style(string);
208 | }
209 |
210 | function style(string) {
211 | return string
212 | .replace(/[\[<]b[\]>]/gi, styles.bold[0])
213 | .replace(/[\[<]\/b[\]>]/gi, styles.bold[1])
214 | .replace(/[\[<]i[\]>]/gi, styles.italic[0])
215 | .replace(/[\[<]\/i[\]>]/gi, styles.italic[1])
216 | .replace(/[\[<]d[\]>]/gi, styles.diminish[0])
217 | .replace(/[\[<]\/d[\]>]/gi, styles.diminish[1])
218 | .replace(/[\[<]u[\]>]/gi, styles.underline[0])
219 | .replace(/[\[<]\/u[\]>]/gi, styles.underline[1])
220 | .replace(/[\[<](bg)?(?:color[=\s])?(black|red|green|yellow|blue|magenta|cyan|white|gray)(bright)?[\]>]/gi, function($0, $1, $2, $3) {
221 | var key = '';
222 | if ($1) key += 'bg';
223 | if ($2) key += $1 ? utils.capitalize($2) : $2.toLowerCase();
224 | if ($3) key += 'Bright';
225 |
226 | return styles[key] || $0;
227 | })
228 | .replace(/[\[<]\/(bg)?(color|black|red|green|yellow|blue|magenta|cyan|white|gray)(bright)?[\]>]/gi, function($0, $1) {
229 | return $1 ? styles.bgColorClose : styles.colorClose;
230 | });
231 | }
232 |
233 | function clean(string) {
234 | return string.replace(styleFlagGlobal, '');
235 | }
236 |
237 | render.style = style;
238 | render.clean = clean;
239 |
240 | module.exports = render;
241 |
--------------------------------------------------------------------------------
/test/files/table.md:
--------------------------------------------------------------------------------
1 | | Feature | Chrome | Edge | Firefox (Gecko) | Internet Explorer | Opera | Safari |
2 | | ----------------------------------- | :------: | :----------: | :--------------: | :--------------------------: | :----: | :----: |
3 | | [title 1] |
4 | | Basic support | 38 | 12[1] | 36 (36) | No support | 25 | 9 |
5 | | Symbol.iterator (@@iterator) | 38 | 12 | 36 (36) | No support | 25 | 9 |
6 | | [title 2] |
7 | | Symbol.unscopables (@@unscopables) | 38 | 12 | 48 (48) | No support | 25 | 9 |
8 | | Symbol.species (@@species) | 51 | ? | 41 (41) | No support | ? | ? |
9 |
10 | --------------
11 |
12 | | [title 1] |
13 | | Basic support | 38 | 12[1] | 36 (36) | No support | 25 | 9 |
14 | | Symbol.iterator (@@iterator) | 38 | 12 | 36 (36) | No support | 25 | 9 |
15 | | [title 2] |
16 | | Symbol.unscopables (@@unscopables) | 38 | 12 | 48 (48) | No support | 25 | 9 |
17 | | Symbol.species (@@species) | 51 | ? | 41 (41) | No support | ? | ? |
18 |
19 | --------------
20 |
21 | [
22 | {
23 | "type": "table",
24 | "widths": [
25 | 34,
26 | 6,
27 | 5,
28 | 15,
29 | 17,
30 | 5,
31 | 7
32 | ],
33 | "headers": [
34 | {
35 | "title": "Feature",
36 | "width": 7,
37 | "align": null
38 | },
39 | {
40 | "title": "Chrome",
41 | "width": 6,
42 | "align": "center"
43 | },
44 | {
45 | "title": "Edge",
46 | "width": 4,
47 | "align": "center"
48 | },
49 | {
50 | "title": "Firefox (Gecko)",
51 | "width": 15,
52 | "align": "center"
53 | },
54 | {
55 | "title": "Internet Explorer",
56 | "width": 17,
57 | "align": "center"
58 | },
59 | {
60 | "title": "Opera",
61 | "width": 5,
62 | "align": "center"
63 | },
64 | {
65 | "title": "Safari",
66 | "width": 7,
67 | "align": null
68 | }
69 | ],
70 | "cells": [
71 | {
72 | "rowIndex": 0,
73 | "type": "title",
74 | "title": "title 1"
75 | },
76 | {
77 | "rowIndex": 1,
78 | "type": "row",
79 | "cols": [
80 | {
81 | "value": "Basic support",
82 | "width": 13,
83 | "colIndex": 0,
84 | "align": null
85 | },
86 | {
87 | "value": "38",
88 | "width": 2,
89 | "colIndex": 1,
90 | "align": "center"
91 | },
92 | {
93 | "value": "12[1]",
94 | "width": 5,
95 | "colIndex": 2,
96 | "align": "center"
97 | },
98 | {
99 | "value": "36 (36)",
100 | "width": 7,
101 | "colIndex": 3,
102 | "align": "center"
103 | },
104 | {
105 | "value": "No support",
106 | "width": 10,
107 | "colIndex": 4,
108 | "align": "center"
109 | },
110 | {
111 | "value": "25",
112 | "width": 2,
113 | "colIndex": 5,
114 | "align": "center"
115 | },
116 | {
117 | "value": "9",
118 | "width": 1,
119 | "colIndex": 6,
120 | "align": null
121 | }
122 | ]
123 | },
124 | {
125 | "rowIndex": 2,
126 | "type": "row",
127 | "cols": [
128 | {
129 | "value": "Symbol.iterator (@@iterator)",
130 | "width": 28,
131 | "colIndex": 0,
132 | "align": null
133 | },
134 | {
135 | "value": "38",
136 | "width": 2,
137 | "colIndex": 1,
138 | "align": "center"
139 | },
140 | {
141 | "value": "12",
142 | "width": 2,
143 | "colIndex": 2,
144 | "align": "center"
145 | },
146 | {
147 | "value": "36 (36)",
148 | "width": 7,
149 | "colIndex": 3,
150 | "align": "center"
151 | },
152 | {
153 | "value": "No support",
154 | "width": 10,
155 | "colIndex": 4,
156 | "align": "center"
157 | },
158 | {
159 | "value": "25",
160 | "width": 2,
161 | "colIndex": 5,
162 | "align": "center"
163 | },
164 | {
165 | "value": "9",
166 | "width": 1,
167 | "colIndex": 6,
168 | "align": null
169 | }
170 | ]
171 | },
172 | {
173 | "rowIndex": 3,
174 | "type": "title",
175 | "title": "title 2"
176 | },
177 | {
178 | "rowIndex": 4,
179 | "type": "row",
180 | "cols": [
181 | {
182 | "value": "Symbol.unscopables (@@unscopables)",
183 | "width": 34,
184 | "colIndex": 0,
185 | "align": null
186 | },
187 | {
188 | "value": "38",
189 | "width": 2,
190 | "colIndex": 1,
191 | "align": "center"
192 | },
193 | {
194 | "value": "12",
195 | "width": 2,
196 | "colIndex": 2,
197 | "align": "center"
198 | },
199 | {
200 | "value": "48 (48)",
201 | "width": 7,
202 | "colIndex": 3,
203 | "align": "center"
204 | },
205 | {
206 | "value": "No support",
207 | "width": 10,
208 | "colIndex": 4,
209 | "align": "center"
210 | },
211 | {
212 | "value": "25",
213 | "width": 2,
214 | "colIndex": 5,
215 | "align": "center"
216 | },
217 | {
218 | "value": "9",
219 | "width": 1,
220 | "colIndex": 6,
221 | "align": null
222 | }
223 | ]
224 | },
225 | {
226 | "rowIndex": 5,
227 | "type": "row",
228 | "cols": [
229 | {
230 | "value": "Symbol.species (@@species)",
231 | "width": 26,
232 | "colIndex": 0,
233 | "align": null
234 | },
235 | {
236 | "value": "51",
237 | "width": 2,
238 | "colIndex": 1,
239 | "align": "center"
240 | },
241 | {
242 | "value": "?",
243 | "width": 1,
244 | "colIndex": 2,
245 | "align": "center"
246 | },
247 | {
248 | "value": "41 (41)",
249 | "width": 7,
250 | "colIndex": 3,
251 | "align": "center"
252 | },
253 | {
254 | "value": "No support",
255 | "width": 10,
256 | "colIndex": 4,
257 | "align": "center"
258 | },
259 | {
260 | "value": "?",
261 | "width": 1,
262 | "colIndex": 5,
263 | "align": "center"
264 | },
265 | {
266 | "value": "?",
267 | "width": 1,
268 | "colIndex": 6,
269 | "align": null
270 | }
271 | ]
272 | }
273 | ]
274 | }
275 | ]
--------------------------------------------------------------------------------