├── .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 | ![image](https://user-images.githubusercontent.com/7875897/121989793-4ab17400-cdcf-11eb-92c2-ea20b79d6cca.png) 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 + '>', '']; 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 | ] --------------------------------------------------------------------------------