├── .github └── workflows │ └── main.yml ├── .gitignore ├── .mocharc.yaml ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs └── images │ ├── bullets-1.png │ ├── display.png │ └── usage.png ├── package-lock.json ├── package.json ├── src ├── .babelrc ├── AnsiPainter.coffee ├── Layout.coffee ├── RenderKid.coffee ├── ansiPainter │ ├── styles.coffee │ └── tags.coffee ├── layout │ ├── Block.coffee │ ├── SpecialString.coffee │ └── block │ │ ├── blockAppendor │ │ ├── Default.coffee │ │ └── _BlockAppendor.coffee │ │ ├── blockPrependor │ │ ├── Default.coffee │ │ └── _BlockPrependor.coffee │ │ ├── lineAppendor │ │ ├── Default.coffee │ │ └── _LineAppendor.coffee │ │ ├── linePrependor │ │ ├── Default.coffee │ │ └── _LinePrependor.coffee │ │ └── lineWrapper │ │ ├── Default.coffee │ │ └── _LineWrapper.coffee ├── renderKid │ ├── Styles.coffee │ ├── styleApplier │ │ ├── _common.coffee │ │ ├── block.coffee │ │ └── inline.coffee │ └── styles │ │ ├── Rule.coffee │ │ ├── StyleSheet.coffee │ │ └── rule │ │ ├── DeclarationBlock.coffee │ │ ├── MixedDeclarationSet.coffee │ │ ├── Selector.coffee │ │ └── declarationBlock │ │ ├── Arbitrary.coffee │ │ ├── Background.coffee │ │ ├── Bullet.coffee │ │ ├── Color.coffee │ │ ├── Display.coffee │ │ ├── Height.coffee │ │ ├── Margin.coffee │ │ ├── MarginBottom.coffee │ │ ├── MarginLeft.coffee │ │ ├── MarginRight.coffee │ │ ├── MarginTop.coffee │ │ ├── Padding.coffee │ │ ├── PaddingBottom.coffee │ │ ├── PaddingLeft.coffee │ │ ├── PaddingRight.coffee │ │ ├── PaddingTop.coffee │ │ ├── Width.coffee │ │ ├── _Declaration.coffee │ │ └── _Length.coffee └── tools.coffee └── test ├── AnsiPainter.coffee ├── Layout.coffee ├── RenderKid.coffee ├── layout ├── Block.coffee └── SpecialString.coffee ├── mochaHelpers.coffee ├── renderKid └── styles │ └── StyleSheet.coffee └── tools.coffee /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x, 14.x, 15.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm run compile 25 | - run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bat 2 | .htaccess 3 | 4 | lib/ 5 | node_modules 6 | pg 7 | 8 | -p 9 | 10 | npm-debug.log 11 | !*.gitkeep 12 | 13 | xeno 14 | 15 | css 16 | .sass-cache -------------------------------------------------------------------------------- /.mocharc.yaml: -------------------------------------------------------------------------------- 1 | require: 'coffeescript/register' 2 | recursive: true 3 | reporter: 'spec' 4 | ui: 'bdd' 5 | timeout: 20000 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.bat 2 | .htaccess 3 | 4 | src/ 5 | node_modules 6 | pg 7 | .github/ 8 | .mocharc.yaml 9 | 10 | -p 11 | 12 | npm-debug.log 13 | !*.gitkeep 14 | 15 | xeno 16 | 17 | sass 18 | .sass-cache 19 | 20 | .travis.yml 21 | test/ 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 12.19.0 5 | 6 | sudo: required 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Renderkid Changelog 2 | 3 | ## `3.0.0` 4 | 5 | * **Breaking change**: Dropped support for Node `<12.x` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aria Minaei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RenderKid 2 | [![Build Status](https://secure.travis-ci.org/AriaMinaei/RenderKid.png)](http://travis-ci.org/AriaMinaei/RenderKid) 3 | 4 | RenderKid allows you to use HTML and CSS to style your CLI output, making it easy to create a beautiful, readable, and consistent look for your nodejs tool. 5 | 6 | ## Installation 7 | 8 | Install with npm: 9 | ``` 10 | $ npm install renderkid 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```coffeescript 16 | RenderKid = require('renderkid') 17 | 18 | r = new RenderKid() 19 | 20 | r.style({ 21 | "ul": { 22 | display: "block" 23 | margin: "2 0 2" 24 | } 25 | 26 | "li": { 27 | display: "block" 28 | marginBottom: "1" 29 | } 30 | 31 | "key": { 32 | color: "grey" 33 | marginRight: "1" 34 | } 35 | 36 | "value": { 37 | color: "bright-white" 38 | } 39 | }) 40 | 41 | output = r.render(" 42 | 56 | ") 57 | 58 | console.log(output) 59 | ``` 60 | 61 | ![screenshot of usage](https://github.com/AriaMinaei/RenderKid/raw/master/docs/images/usage.png) 62 | 63 | ## Stylesheet properties 64 | 65 | ### Display mode 66 | 67 | Elements can have a `display` of either `inline`, `block`, or `none`: 68 | ```coffeescript 69 | r.style({ 70 | "div": { 71 | display: "block" 72 | } 73 | 74 | "span": { 75 | display: "inline" # default 76 | } 77 | 78 | "hidden": { 79 | display: "none" 80 | } 81 | }) 82 | 83 | output = r.render(" 84 |
This will fill one or more rows.
85 | These will be in the same line. 86 | This won't be displayed. 87 | ") 88 | 89 | console.log(output) 90 | ``` 91 | 92 | ![screenshot of usage](https://github.com/AriaMinaei/RenderKid/raw/master/docs/images/display.png) 93 | 94 | 95 | ### Margin 96 | 97 | Margins work just like they do in browsers: 98 | ```coffeescript 99 | r.style({ 100 | "li": { 101 | display: "block" 102 | 103 | marginTop: "1" 104 | marginRight: "2" 105 | marginBottom: "3" 106 | marginLeft: "4" 107 | 108 | # or the shorthand version: 109 | "margin": "1 2 3 4" 110 | }, 111 | 112 | "highlight": { 113 | display: "inline" 114 | marginLeft: "2" 115 | marginRight: "2" 116 | } 117 | }) 118 | 119 | r.render(" 120 | 125 | ") 126 | ``` 127 | 128 | ### Padding 129 | 130 | See margins above. Paddings work the same way, only inward. 131 | 132 | ### Width and Height 133 | 134 | Block elements can have explicit width and height: 135 | ```coffeescript 136 | r.style({ 137 | "box": { 138 | display: "block" 139 | "width": "4" 140 | "height": "2" 141 | } 142 | }) 143 | 144 | r.render("This is a box and some of its text will be truncated.") 145 | ``` 146 | 147 | ### Colors 148 | 149 | You can set a custom color and background color for each element: 150 | 151 | ```coffeescript 152 | r.style({ 153 | "error": { 154 | color: "black" 155 | background: "red" 156 | } 157 | }) 158 | ``` 159 | 160 | List of colors currently supported are `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `grey`, `bright-red`, `bright-green`, `bright-yellow`, `bright-blue`, `bright-magenta`, `bright-cyan`, `bright-white`. 161 | 162 | ### Bullet points 163 | 164 | Block elements can have bullet points on their margins. Let's start with an example: 165 | ```coffeescript 166 | r.style({ 167 | "li": { 168 | # To add bullet points to an element, first you 169 | # should make some room for the bullet point by 170 | # giving your element some margin to the left: 171 | marginLeft: "4", 172 | 173 | # Now we can add a bullet point to our margin: 174 | bullet: '"-"' 175 | } 176 | }) 177 | 178 | # The four hyphens are there for visual reference 179 | r.render(" 180 | ---- 181 |
  • Item 1
  • 182 |
  • Item 2
  • 183 |
  • Item 3
  • 184 | ---- 185 | ") 186 | ``` 187 | And here is the result: 188 | 189 | ![screenshot of bullet points, 1](https://github.com/AriaMinaei/RenderKid/raw/master/docs/images/bullets-1.png) 190 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.0.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Send security alerts to aria@theatrejs.com 12 | -------------------------------------------------------------------------------- /docs/images/bullets-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AriaMinaei/RenderKid/05c6f764d4e4e421c3152229655eee695833b90f/docs/images/bullets-1.png -------------------------------------------------------------------------------- /docs/images/display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AriaMinaei/RenderKid/05c6f764d4e4e421c3152229655eee695833b90f/docs/images/display.png -------------------------------------------------------------------------------- /docs/images/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AriaMinaei/RenderKid/05c6f764d4e4e421c3152229655eee695833b90f/docs/images/usage.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "renderkid", 3 | "version": "3.0.0", 4 | "description": "Stylish console.log for node", 5 | "main": "lib/RenderKid.js", 6 | "dependencies": { 7 | "css-select": "^4.1.3", 8 | "dom-converter": "^0.2.0", 9 | "htmlparser2": "^6.1.0", 10 | "lodash": "^4.17.21", 11 | "strip-ansi": "^6.0.1" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.14.5", 15 | "@babel/preset-env": "^7.14.5", 16 | "chai": "^4.3.4", 17 | "chai-changes": "^1.3.6", 18 | "chai-fuzzy": "^1.6.1", 19 | "coffeescript": "^2.5.1", 20 | "mocha": "^9.1.3", 21 | "sinon": "^11.1.1", 22 | "sinon-chai": "^3.7.0" 23 | }, 24 | "scripts": { 25 | "test": "mocha \"test/**/*.coffee\"", 26 | "test:watch": "npm run test -- --watch", 27 | "compile": "coffee --bare --transpile --output ./lib ./src", 28 | "compile:watch": "coffee --watch --bare --transpile --output ./lib ./src", 29 | "watch": "npm run compile:watch & npm run test:watch", 30 | "prepublish": "npm run compile" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/AriaMinaei/RenderKid.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/AriaMinaei/RenderKid/issues" 38 | }, 39 | "author": "Aria Minaei", 40 | "license": "MIT" 41 | } 42 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "0.10" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/AnsiPainter.coffee: -------------------------------------------------------------------------------- 1 | tools = require './tools' 2 | tags = require './ansiPainter/tags' 3 | styles = require './ansiPainter/styles' 4 | 5 | module.exports = class AnsiPainter 6 | @tags: tags 7 | 8 | paint: (s) -> 9 | @_replaceSpecialStrings @_renderDom @_parse s 10 | 11 | _replaceSpecialStrings: (str) -> 12 | str 13 | .replace(/&sp;/g, ' ') 14 | .replace(/</g, '<') 15 | .replace(/>/g, '>') 16 | .replace(/"/g, '"') 17 | .replace(/&/g, '&') 18 | 19 | _parse: (string, injectFakeRoot = yes) -> 20 | if injectFakeRoot then string = '' + string + '' 21 | tools.toDom string 22 | 23 | _renderDom: (dom) -> 24 | parentStyles = bg: 'none', color: 'none' 25 | @_renderChildren dom, parentStyles 26 | 27 | _renderChildren: (children, parentStyles) -> 28 | ret = '' 29 | for own n, child of children 30 | ret += @_renderNode child, parentStyles 31 | 32 | ret 33 | 34 | _renderNode: (node, parentStyles) -> 35 | if node.type is 'text' 36 | @_renderTextNode node, parentStyles 37 | else 38 | @_renderTag node, parentStyles 39 | 40 | _renderTextNode: (node, parentStyles) -> 41 | @_wrapInStyle node.data, parentStyles 42 | 43 | _wrapInStyle: (str, style) -> 44 | styles.color(style.color) + styles.bg(style.bg) + str + styles.none() 45 | 46 | _renderTag: (node, parentStyles) -> 47 | tagStyles = @_getStylesForTagName node.name 48 | currentStyles = @_mixStyles parentStyles, tagStyles 49 | @_renderChildren node.children, currentStyles 50 | 51 | _mixStyles: (styles...) -> 52 | final = {} 53 | for style in styles 54 | for own key, val of style 55 | if not final[key]? or val isnt 'inherit' 56 | final[key] = val 57 | 58 | final 59 | 60 | _getStylesForTagName: (name) -> 61 | unless tags[name]? 62 | throw Error "Unknown tag name `#{name}`" 63 | 64 | tags[name] 65 | 66 | self = @ 67 | 68 | @getInstance: -> 69 | unless self._instance? 70 | self._instance = new self 71 | self._instance 72 | 73 | @paint: (str) -> 74 | self.getInstance().paint str 75 | 76 | @strip: (s) -> 77 | s.replace(/\x1b\[[0-9]+m/g, '') -------------------------------------------------------------------------------- /src/Layout.coffee: -------------------------------------------------------------------------------- 1 | Block = require './layout/Block' 2 | {cloneAndMergeDeep} = require './tools' 3 | SpecialString = require './layout/SpecialString' 4 | terminalWidth = require('./tools').getCols() 5 | 6 | module.exports = class Layout 7 | self = @ 8 | 9 | @_rootBlockDefaultConfig: 10 | linePrependor: options: amount: 0 11 | lineAppendor: options: amount: 0 12 | blockPrependor: options: amount: 0 13 | blockAppendor: options: amount: 0 14 | 15 | @_defaultConfig: 16 | terminalWidth: terminalWidth 17 | 18 | constructor: (config = {}, rootBlockConfig = {}) -> 19 | @_written = [] 20 | @_activeBlock = null 21 | @_config = cloneAndMergeDeep self._defaultConfig, config 22 | 23 | # Every layout has a root block 24 | rootConfig = cloneAndMergeDeep self._rootBlockDefaultConfig, rootBlockConfig 25 | @_root = new Block @, null, rootConfig, '__root' 26 | @_root._open() 27 | 28 | getRootBlock: -> 29 | @_root 30 | 31 | _append: (text) -> 32 | @_written.push text 33 | 34 | _appendLine: (text) -> 35 | @_append text 36 | s = new SpecialString(text) 37 | if s.length < @_config.terminalWidth 38 | @_append '\n' 39 | 40 | this 41 | 42 | get: -> 43 | do @_ensureClosed 44 | if @_written[@_written.length - 1] is '\n' 45 | @_written.pop() 46 | @_written.join "" 47 | 48 | _ensureClosed: -> 49 | if @_activeBlock isnt @_root 50 | throw Error "Not all the blocks have been closed. 51 | Please call block.close() on all open blocks." 52 | 53 | if @_root.isOpen() 54 | @_root.close() 55 | 56 | return 57 | 58 | for prop in ['openBlock', 'write'] then do -> 59 | method = prop 60 | Layout::[method] = -> 61 | @_root[method].apply @_root, arguments -------------------------------------------------------------------------------- /src/RenderKid.coffee: -------------------------------------------------------------------------------- 1 | inlineStyleApplier = require './renderKid/styleApplier/inline' 2 | blockStyleApplier = require './renderKid/styleApplier/block' 3 | isPlainObject = require 'lodash/isPlainObject' 4 | {cloneAndMergeDeep} = require './tools' 5 | AnsiPainter = require './AnsiPainter' 6 | Styles = require './renderKid/Styles' 7 | Layout = require './Layout' 8 | tools = require './tools' 9 | stripAnsi = require 'strip-ansi' 10 | terminalWidth = require('./tools').getCols() 11 | 12 | module.exports = class RenderKid 13 | self = @ 14 | @AnsiPainter: AnsiPainter 15 | @Layout: Layout 16 | @quote: tools.quote 17 | @tools: tools 18 | @_defaultConfig: 19 | layout: {terminalWidth: terminalWidth} 20 | 21 | constructor: (config = {}) -> 22 | @tools = self.tools 23 | @_config = cloneAndMergeDeep self._defaultConfig, config 24 | do @_initStyles 25 | 26 | _initStyles: -> 27 | @_styles = new Styles 28 | 29 | style: -> 30 | @_styles.setRule.apply @_styles, arguments 31 | 32 | _getStyleFor: (el) -> 33 | @_styles.getStyleFor el 34 | 35 | render: (input, withColors = yes) -> 36 | @_paint (@_renderDom @_toDom input), withColors 37 | 38 | _toDom: (input) -> 39 | if typeof input is 'string' 40 | @_parse input 41 | else if isPlainObject(input) or Array.isArray(input) 42 | @_objToDom input 43 | else 44 | throw Error "Invalid input type. Only strings, arrays and objects are accepted" 45 | 46 | _objToDom: (o, injectFakeRoot = yes) -> 47 | if injectFakeRoot then o = body: o 48 | tools.objectToDom o 49 | 50 | _paint: (text, withColors) -> 51 | painted = AnsiPainter.paint(text) 52 | 53 | if withColors 54 | painted 55 | else 56 | stripAnsi painted 57 | 58 | _parse: (string, injectFakeRoot = yes) -> 59 | if injectFakeRoot then string = '' + string + '' 60 | tools.stringToDom string 61 | 62 | _renderDom: (dom) -> 63 | bodyTag = dom[0] 64 | layout = new Layout @_config.layout 65 | rootBlock = layout.getRootBlock() 66 | @_renderBlockNode bodyTag, null, rootBlock 67 | layout.get() 68 | 69 | _renderChildrenOf: (parentNode, parentBlock) -> 70 | nodes = parentNode.children 71 | for node in nodes 72 | @_renderNode node, parentNode, parentBlock 73 | 74 | return 75 | 76 | _renderNode: (node, parentNode, parentBlock) -> 77 | if node.type is 'text' 78 | @_renderText node, parentNode, parentBlock 79 | else if node.name is 'br' 80 | @_renderBr node, parentNode, parentBlock 81 | else if @_isBlock node 82 | @_renderBlockNode node, parentNode, parentBlock 83 | else if @_isNone node 84 | return 85 | else 86 | @_renderInlineNode node, parentNode, parentBlock 87 | 88 | return 89 | 90 | _renderText: (node, parentNode, parentBlock) -> 91 | text = node.data 92 | text = text.replace /\s+/g, ' ' 93 | 94 | # let's only trim if the parent is an inline element 95 | unless parentNode?.styles?.display is 'inline' 96 | text = text.trim() 97 | 98 | return if text.length is 0 99 | 100 | text = text.replace /&nl;/g, "\n" 101 | parentBlock.write text 102 | 103 | _renderBlockNode: (node, parentNode, parentBlock) -> 104 | {before, after, blockConfig} = 105 | blockStyleApplier.applyTo node, @_getStyleFor node 106 | 107 | block = parentBlock.openBlock(blockConfig) 108 | if before isnt '' then block.write before 109 | @_renderChildrenOf node, block 110 | if after isnt '' then block.write after 111 | block.close() 112 | 113 | _renderInlineNode: (node, parentNode, parentBlock) -> 114 | {before, after} = inlineStyleApplier.applyTo node, @_getStyleFor node 115 | if before isnt '' then parentBlock.write before 116 | @_renderChildrenOf node, parentBlock 117 | if after isnt '' then parentBlock.write after 118 | 119 | _renderBr: (node, parentNode, parentBlock) -> 120 | parentBlock.write "\n" 121 | 122 | _isBlock: (node) -> 123 | not(node.type is 'text' or node.name is 'br' or @_getStyleFor(node).display isnt 'block') 124 | 125 | _isNone: (node) -> 126 | not (node.type is 'text' or node.name is 'br' or @_getStyleFor(node).display isnt 'none') -------------------------------------------------------------------------------- /src/ansiPainter/styles.coffee: -------------------------------------------------------------------------------- 1 | module.exports = styles = {} 2 | 3 | styles.codes = codes = 4 | 'none': 0 5 | 6 | 'black': 30 7 | 'red': 31 8 | 'green': 32 9 | 'yellow': 33 10 | 'blue': 34 11 | 'magenta': 35 12 | 'cyan': 36 13 | 'white': 37 14 | 15 | 'grey': 90 16 | 'bright-red': 91 17 | 'bright-green': 92 18 | 'bright-yellow': 93 19 | 'bright-blue': 94 20 | 'bright-magenta': 95 21 | 'bright-cyan': 96 22 | 'bright-white': 97 23 | 24 | 'bg-black': 40 25 | 'bg-red': 41 26 | 'bg-green': 42 27 | 'bg-yellow': 43 28 | 'bg-blue': 44 29 | 'bg-magenta': 45 30 | 'bg-cyan': 46 31 | 'bg-white': 47 32 | 33 | 'bg-grey': 100 34 | 'bg-bright-red': 101 35 | 'bg-bright-green': 102 36 | 'bg-bright-yellow': 103 37 | 'bg-bright-blue': 104 38 | 'bg-bright-magenta': 105 39 | 'bg-bright-cyan': 106 40 | 'bg-bright-white': 107 41 | 42 | styles.color = (str) -> 43 | return '' if str is 'none' 44 | code = codes[str] 45 | unless code? 46 | throw Error "Unknown color `#{str}`" 47 | 48 | "\x1b[" + code + "m" 49 | 50 | styles.bg = (str) -> 51 | return '' if str is 'none' 52 | code = codes['bg-' + str] 53 | unless code? 54 | throw Error "Unknown bg color `#{str}`" 55 | 56 | "\x1B[" + code + "m" 57 | 58 | styles.none = (str) -> 59 | "\x1B[" + codes.none + "m" -------------------------------------------------------------------------------- /src/ansiPainter/tags.coffee: -------------------------------------------------------------------------------- 1 | module.exports = tags = 2 | 'none': color: 'none', bg: 'none' 3 | 'bg-none': color: 'inherit', bg: 'none' 4 | 'color-none': color: 'none', bg: 'inherit' 5 | 6 | colors = [ 7 | 'black' 8 | 'red' 9 | 'green' 10 | 'yellow' 11 | 'blue' 12 | 'magenta' 13 | 'cyan' 14 | 'white' 15 | 16 | 'grey' 17 | 'bright-red' 18 | 'bright-green' 19 | 'bright-yellow' 20 | 'bright-blue' 21 | 'bright-magenta' 22 | 'bright-cyan' 23 | 'bright-white' 24 | ] 25 | 26 | for color in colors 27 | tags[color] = color: color, bg: 'inherit' 28 | tags["color-#{color}"] = color: color, bg: 'inherit' 29 | tags["bg-#{color}"] = color: 'inherit', bg: color -------------------------------------------------------------------------------- /src/layout/Block.coffee: -------------------------------------------------------------------------------- 1 | SpecialString = require './SpecialString' 2 | terminalWidth = require('../tools').getCols() 3 | {cloneAndMergeDeep} = require '../tools' 4 | 5 | module.exports = class Block 6 | self = @ 7 | 8 | @defaultConfig = 9 | blockPrependor: 10 | fn: require './block/blockPrependor/Default' 11 | options: amount: 0 12 | 13 | blockAppendor: 14 | fn: require './block/blockAppendor/Default' 15 | options: amount: 0 16 | 17 | linePrependor: 18 | fn: require './block/linePrependor/Default' 19 | options: amount: 0 20 | 21 | lineAppendor: 22 | fn: require './block/lineAppendor/Default' 23 | options: amount: 0 24 | 25 | lineWrapper: 26 | fn: require './block/lineWrapper/Default' 27 | options: lineWidth: null 28 | 29 | width: terminalWidth 30 | 31 | prefixRaw: '' 32 | suffixRaw: '' 33 | 34 | constructor: (@_layout, @_parent, config = {}, @_name = '') -> 35 | @_config = cloneAndMergeDeep self.defaultConfig, config 36 | @_closed = no 37 | @_wasOpenOnce = no 38 | @_active = no 39 | @_buffer = '' 40 | @_didSeparateBlock = no 41 | 42 | @_linePrependor = 43 | new @_config.linePrependor.fn @_config.linePrependor.options 44 | 45 | @_lineAppendor = 46 | new @_config.lineAppendor.fn @_config.lineAppendor.options 47 | 48 | @_blockPrependor = 49 | new @_config.blockPrependor.fn @_config.blockPrependor.options 50 | 51 | @_blockAppendor = 52 | new @_config.blockAppendor.fn @_config.blockAppendor.options 53 | 54 | _activate: (deactivateParent = yes) -> 55 | if @_active 56 | throw Error "This block is already active. This is probably a bug in RenderKid itself" 57 | 58 | if @_closed 59 | throw Error "This block is closed and cannot be activated. This is probably a bug in RenderKid itself" 60 | 61 | @_active = yes 62 | @_layout._activeBlock = @ 63 | if deactivateParent 64 | if @_parent? then @_parent._deactivate no 65 | 66 | this 67 | 68 | _deactivate: (activateParent = yes) -> 69 | do @_ensureActive 70 | do @_flushBuffer 71 | 72 | if activateParent 73 | if @_parent? then @_parent._activate no 74 | 75 | @_active = no 76 | 77 | this 78 | 79 | _ensureActive: -> 80 | unless @_wasOpenOnce 81 | throw Error "This block has never been open before. This is probably a bug in RenderKid itself." 82 | 83 | unless @_active 84 | throw Error "This block is not active. This is probably a bug in RenderKid itself." 85 | 86 | if @_closed 87 | throw Error "This block is already closed. This is probably a bug in RenderKid itself." 88 | 89 | _open: -> 90 | if @_wasOpenOnce 91 | throw Error "Block._open() has been called twice. This is probably a RenderKid bug." 92 | 93 | @_wasOpenOnce = yes 94 | 95 | if @_parent? then @_parent.write @_whatToPrependToBlock() 96 | do @_activate 97 | this 98 | 99 | close: -> 100 | do @_deactivate 101 | @_closed = yes 102 | if @_parent? then @_parent.write @_whatToAppendToBlock() 103 | this 104 | 105 | isOpen: -> 106 | @_wasOpenOnce and not @_closed 107 | 108 | write: (str) -> 109 | do @_ensureActive 110 | return if str is '' 111 | str = String str 112 | @_buffer += str 113 | this 114 | 115 | openBlock: (config, name) -> 116 | do @_ensureActive 117 | block = new Block @_layout, @, config, name 118 | block._open() 119 | block 120 | 121 | _flushBuffer: -> 122 | return if @_buffer is '' 123 | str = @_buffer 124 | @_buffer = '' 125 | @_writeInline str 126 | return 127 | 128 | _toPrependToLine: -> 129 | fromParent = '' 130 | if @_parent? 131 | fromParent = @_parent._toPrependToLine() 132 | 133 | @_linePrependor.render fromParent 134 | 135 | _toAppendToLine: -> 136 | fromParent = '' 137 | if @_parent? 138 | fromParent = @_parent._toAppendToLine() 139 | 140 | @_lineAppendor.render fromParent 141 | 142 | _whatToPrependToBlock: -> 143 | @_blockPrependor.render() 144 | 145 | _whatToAppendToBlock: -> 146 | @_blockAppendor.render() 147 | 148 | _writeInline: (str) -> 149 | # special characters (such as ) don't require 150 | # any wrapping... 151 | if new SpecialString(str).isOnlySpecialChars() 152 | # ... and directly get appended to the layout. 153 | @_layout._append str 154 | return 155 | 156 | # we'll be removing from the original string till it's empty 157 | remaining = str 158 | 159 | # we might need to add a few line breaks at the end of the text. 160 | lineBreaksToAppend = 0 161 | 162 | # if text starts with line breaks... 163 | if m = remaining.match /^\n+/ 164 | 165 | # ... we want to write the exact same number of line breaks 166 | # to the layout. 167 | for i in [1..m[0].length] 168 | @_writeLine '' 169 | 170 | remaining = remaining.substr m[0].length, remaining.length 171 | 172 | # and if the text ends with line breaks... 173 | if m = remaining.match /\n+$/ 174 | # we want to write the exact same number of line breaks 175 | # to the end of the layout. 176 | lineBreaksToAppend = m[0].length 177 | remaining = remaining.substr 0, remaining.length - m[0].length 178 | 179 | # now let's parse the body of the text: 180 | while remaining.length > 0 181 | # anything other than a break line... 182 | if m = remaining.match /^[^\n]+/ 183 | # ... should be wrapped as a block of text. 184 | @_writeLine m[0] 185 | 186 | remaining = remaining.substr m[0].length, remaining.length 187 | 188 | # for any number of line breaks we find inside the text... 189 | else if m = remaining.match /^\n+/ 190 | # ... we write one less break line to the layout. 191 | for i in [1...m[0].length] 192 | @_writeLine '' 193 | 194 | remaining = remaining.substr m[0].length, remaining.length 195 | 196 | # if we had line breaks to append to the layout... 197 | if lineBreaksToAppend > 0 198 | # ... we append the exact same number of line breaks to the layout. 199 | for i in [1..lineBreaksToAppend] 200 | @_writeLine '' 201 | 202 | return 203 | 204 | # wraps a line into multiple lines if necessary, adds horizontal margins, 205 | # etc, and appends it to the layout. 206 | _writeLine: (str) -> 207 | # we'll be cutting from our string as we go 208 | remaining = new SpecialString str 209 | 210 | # this will continue until nothing is left of our block. 211 | loop 212 | # left margin... 213 | toPrepend = @_toPrependToLine() 214 | 215 | # ... and its length 216 | toPrependLength = new SpecialString(toPrepend).length 217 | 218 | # right margin... 219 | toAppend = @_toAppendToLine() 220 | 221 | # ... and its length 222 | toAppendLength = new SpecialString(toAppend).length 223 | 224 | # how much room is left for content 225 | roomLeft = @_layout._config.terminalWidth - (toPrependLength + toAppendLength) 226 | 227 | # how much room each line of content will have 228 | lineContentLength = Math.min @_config.width, roomLeft 229 | 230 | # cut line content, only for the amount needed 231 | lineContent = remaining.cut(0, lineContentLength, yes) 232 | 233 | # line will consist of both margins and the content 234 | line = toPrepend + lineContent.str + toAppend 235 | 236 | # send it off to layout 237 | @_layout._appendLine line 238 | 239 | break if remaining.isEmpty() 240 | 241 | return -------------------------------------------------------------------------------- /src/layout/SpecialString.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class SpecialString 2 | self = @ 3 | 4 | @_tabRx: /^\t/ 5 | 6 | @_tagRx: /// 7 | ^ 8 | < 9 | [^>]+ 10 | > 11 | /// 12 | 13 | @_quotedHtmlRx: /// 14 | ^ 15 | & 16 | (gt|lt|quot|amp|apos|sp) 17 | ; 18 | /// 19 | 20 | constructor: (str) -> 21 | unless this instanceof self 22 | return new self str 23 | 24 | @_str = String str 25 | @_len = 0 26 | 27 | _getStr: -> 28 | @_str 29 | 30 | set: (str) -> 31 | @_str = String str 32 | this 33 | 34 | clone: -> 35 | new SpecialString @_str 36 | 37 | isEmpty: -> 38 | @_str is '' 39 | 40 | isOnlySpecialChars: -> 41 | not @isEmpty() and @length is 0 42 | 43 | _reset: -> 44 | @_len = 0 45 | 46 | splitIn: (limit, trimLeftEachLine = no) -> 47 | buffer = '' 48 | bufferLength = 0 49 | lines = [] 50 | justSkippedSkipChar = no 51 | self._countChars @_str, (char, charLength) -> 52 | if bufferLength > limit or bufferLength + charLength > limit 53 | lines.push buffer 54 | buffer = '' 55 | bufferLength = 0 56 | 57 | if bufferLength is 0 and char is ' ' and not justSkippedSkipChar and trimLeftEachLine 58 | justSkippedSkipChar = yes 59 | else 60 | buffer += char 61 | bufferLength += charLength 62 | justSkippedSkipChar = no 63 | 64 | if buffer.length > 0 65 | lines.push buffer 66 | 67 | lines 68 | 69 | trim: -> 70 | new SpecialString(@str.trim()) 71 | 72 | _getLength: -> 73 | sum = 0 74 | self._countChars @_str, (char, charLength) -> 75 | sum += charLength 76 | return 77 | 78 | sum 79 | 80 | cut: (from, to, trimLeft = no) -> 81 | unless to? then to = @length 82 | from = parseInt from 83 | if from >= to 84 | throw Error "`from` shouldn't be larger than `to`" 85 | 86 | before = '' 87 | after = '' 88 | cut = '' 89 | cur = 0 90 | 91 | self._countChars @_str, (char, charLength) => 92 | if @str is 'ab' 93 | console.log charLength, char 94 | 95 | return if cur is from and char.match(/^\s+$/) and trimLeft 96 | 97 | if cur < from 98 | before += char 99 | # let's be greedy 100 | else if cur < to or cur + charLength <= to 101 | cut += char 102 | else 103 | after += char 104 | 105 | cur += charLength 106 | return 107 | 108 | @_str = before + after 109 | do @_reset 110 | new SpecialString cut 111 | 112 | @_countChars: (text, cb) -> 113 | while text.length isnt 0 114 | if m = text.match self._tagRx 115 | char = m[0] 116 | charLength = 0 117 | text = text.substr char.length, text.length 118 | else if m = text.match self._quotedHtmlRx 119 | char = m[0] 120 | charLength = 1 121 | text = text.substr char.length, text.length 122 | else if text.match self._tabRx 123 | char = "\t" 124 | charLength = 8 125 | text = text.substr 1, text.length 126 | else 127 | char = text[0] 128 | charLength = 1 129 | text = text.substr 1, text.length 130 | 131 | cb.call null, char, charLength 132 | 133 | return 134 | 135 | for prop in ['str', 'length'] then do -> 136 | methodName = '_get' + prop[0].toUpperCase() + prop.substr(1, prop.length) 137 | SpecialString::__defineGetter__ prop, -> do @[methodName] -------------------------------------------------------------------------------- /src/layout/block/blockAppendor/Default.coffee: -------------------------------------------------------------------------------- 1 | tools = require '../../../tools' 2 | 3 | module.exports = class DefaultBlockAppendor extends require './_BlockAppendor' 4 | _render: (options) -> 5 | tools.repeatString "\n", @_config.amount -------------------------------------------------------------------------------- /src/layout/block/blockAppendor/_BlockAppendor.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class _BlockAppendor 2 | constructor: (@_config) -> 3 | 4 | render: (options) -> 5 | @_render(options) -------------------------------------------------------------------------------- /src/layout/block/blockPrependor/Default.coffee: -------------------------------------------------------------------------------- 1 | tools = require '../../../tools' 2 | 3 | module.exports = class DefaultBlockPrependor extends require './_BlockPrependor' 4 | _render: (options) -> 5 | tools.repeatString "\n", @_config.amount -------------------------------------------------------------------------------- /src/layout/block/blockPrependor/_BlockPrependor.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class _BlockPrependor 2 | constructor: (@_config) -> 3 | 4 | render: (options) -> 5 | @_render(options) -------------------------------------------------------------------------------- /src/layout/block/lineAppendor/Default.coffee: -------------------------------------------------------------------------------- 1 | tools = require '../../../tools' 2 | 3 | module.exports = class DefaultLineAppendor extends require './_LineAppendor' 4 | _render: (inherited, options) -> 5 | inherited + tools.repeatString " ", @_config.amount -------------------------------------------------------------------------------- /src/layout/block/lineAppendor/_LineAppendor.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class _LineAppendor 2 | constructor: (@_config) -> 3 | @_lineNo = 0 4 | 5 | render: (inherited, options) -> 6 | @_lineNo++ 7 | '' + @_render(inherited, options) + '' -------------------------------------------------------------------------------- /src/layout/block/linePrependor/Default.coffee: -------------------------------------------------------------------------------- 1 | tools = require '../../../tools' 2 | SpecialString = require '../../SpecialString' 3 | 4 | module.exports = class DefaultLinePrependor extends require './_LinePrependor' 5 | self = @ 6 | 7 | @pad: (howMuch) -> 8 | tools.repeatString(" ", howMuch) 9 | 10 | _render: (inherited, options) -> 11 | if @_lineNo is 0 and bullet = @_config.bullet 12 | char = bullet.char 13 | charLen = new SpecialString(char).length 14 | alignment = bullet.alignment 15 | space = @_config.amount 16 | toWrite = char 17 | addToLeft = '' 18 | addToRight = '' 19 | 20 | if space > charLen 21 | diff = space - charLen 22 | if alignment is 'right' 23 | addToLeft = self.pad diff 24 | else if alignment is 'left' 25 | addToRight = self.pad(diff) 26 | else if alignment is 'center' 27 | left = Math.round diff / 2 28 | addToLeft = self.pad left 29 | addToRight = self.pad diff - left 30 | else 31 | throw Error "Unknown alignment `#{alignment}`" 32 | output = addToLeft + char + addToRight 33 | else 34 | output = self.pad @_config.amount 35 | 36 | inherited + output -------------------------------------------------------------------------------- /src/layout/block/linePrependor/_LinePrependor.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class _LinePrependor 2 | constructor: (@_config) -> 3 | @_lineNo = -1 4 | 5 | render: (inherited, options) -> 6 | @_lineNo++ 7 | '' + @_render(inherited, options) + '' -------------------------------------------------------------------------------- /src/layout/block/lineWrapper/Default.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class DefaultLineWrapper extends require './_LineWrapper' 2 | 3 | _render: -> -------------------------------------------------------------------------------- /src/layout/block/lineWrapper/_LineWrapper.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class _LineWrapper 2 | constructor: -> 3 | 4 | render: (str, options) -> 5 | @_render str, options -------------------------------------------------------------------------------- /src/renderKid/Styles.coffee: -------------------------------------------------------------------------------- 1 | StyleSheet = require './styles/StyleSheet' 2 | MixedDeclarationSet = require './styles/rule/MixedDeclarationSet' 3 | terminalWidth = require('../tools').getCols() 4 | 5 | module.exports = class Styles 6 | self = @ 7 | 8 | @defaultRules: 9 | '*': 10 | display: 'inline' 11 | 'body': 12 | background: 'none' 13 | color: 'white' 14 | display: 'block' 15 | width: terminalWidth + ' !important' 16 | 17 | constructor: -> 18 | @_defaultStyles = new StyleSheet 19 | @_userStyles = new StyleSheet 20 | do @_setDefaultStyles 21 | 22 | _setDefaultStyles: -> 23 | @_defaultStyles.setRule self.defaultRules 24 | return 25 | 26 | setRule: (selector, rules) -> 27 | @_userStyles.setRule.apply @_userStyles, arguments 28 | this 29 | 30 | getStyleFor: (el) -> 31 | styles = el.styles 32 | unless styles? 33 | el.styles = styles = @_getComputedStyleFor el 34 | styles 35 | 36 | _getRawStyleFor: (el) -> 37 | def = @_defaultStyles.getRulesFor el 38 | user = @_userStyles.getRulesFor el 39 | MixedDeclarationSet.mix(def, user).toObject() 40 | 41 | _getComputedStyleFor: (el) -> 42 | decs = {} 43 | parent = el.parent 44 | for prop, val of @_getRawStyleFor el 45 | unless val is 'inherit' 46 | decs[prop] = val 47 | else 48 | throw Error "Inherited styles are not supported yet." 49 | 50 | decs -------------------------------------------------------------------------------- /src/renderKid/styleApplier/_common.coffee: -------------------------------------------------------------------------------- 1 | AnsiPainter = require '../../AnsiPainter' 2 | 3 | module.exports = _common = 4 | getStyleTagsFor: (style) -> 5 | tagsToAdd = [] 6 | 7 | if style.color? 8 | tagName = 'color-' + style.color 9 | unless AnsiPainter.tags[tagName]? 10 | throw Error "Unknown color `#{style.color}`" 11 | 12 | tagsToAdd.push tagName 13 | 14 | if style.background? 15 | tagName = 'bg-' + style.background 16 | unless AnsiPainter.tags[tagName]? 17 | throw Error "Unknown background `#{style.background}`" 18 | 19 | tagsToAdd.push tagName 20 | 21 | ret = before: '', after: '' 22 | 23 | for tag in tagsToAdd 24 | ret.before = "<#{tag}>" + ret.before 25 | ret.after = ret.after + "" 26 | 27 | ret -------------------------------------------------------------------------------- /src/renderKid/styleApplier/block.coffee: -------------------------------------------------------------------------------- 1 | _common = require './_common' 2 | merge = require 'lodash/merge' 3 | 4 | module.exports = blockStyleApplier = self = 5 | applyTo: (el, style) -> 6 | ret = _common.getStyleTagsFor style 7 | ret.blockConfig = config = {} 8 | @_margins style, config 9 | @_bullet style, config 10 | @_dims style, config 11 | ret 12 | 13 | _margins: (style, config) -> 14 | if style.marginLeft? 15 | merge config, linePrependor: 16 | options: amount: parseInt style.marginLeft 17 | 18 | if style.marginRight? 19 | merge config, lineAppendor: 20 | options: amount: parseInt style.marginRight 21 | 22 | if style.marginTop? 23 | merge config, blockPrependor: 24 | options: amount: parseInt style.marginTop 25 | 26 | if style.marginBottom? 27 | merge config, blockAppendor: 28 | options: amount: parseInt style.marginBottom 29 | 30 | return 31 | 32 | _bullet: (style, config) -> 33 | if style.bullet? and style.bullet.enabled 34 | bullet = style.bullet 35 | conf = {} 36 | conf.alignment = style.bullet.alignment 37 | {before, after} = _common.getStyleTagsFor color: bullet.color, background: bullet.background 38 | conf.char = before + bullet.char + after 39 | merge config, linePrependor: 40 | options: bullet: conf 41 | 42 | return 43 | 44 | _dims: (style, config) -> 45 | if style.width? 46 | w = parseInt style.width 47 | config.width = w 48 | 49 | return -------------------------------------------------------------------------------- /src/renderKid/styleApplier/inline.coffee: -------------------------------------------------------------------------------- 1 | tools = require '../../tools' 2 | _common = require './_common' 3 | 4 | module.exports = inlineStyleApplier = self = 5 | applyTo: (el, style) -> 6 | ret = _common.getStyleTagsFor style 7 | 8 | if style.marginLeft? 9 | ret.before = (tools.repeatString "&sp;", parseInt style.marginLeft) + ret.before 10 | 11 | if style.marginRight? 12 | ret.after += tools.repeatString "&sp;", parseInt style.marginRight 13 | 14 | if style.paddingLeft? 15 | ret.before += tools.repeatString "&sp;", parseInt style.paddingLeft 16 | 17 | if style.paddingRight? 18 | ret.after = (tools.repeatString "&sp;", parseInt style.paddingRight) + ret.after 19 | 20 | ret -------------------------------------------------------------------------------- /src/renderKid/styles/Rule.coffee: -------------------------------------------------------------------------------- 1 | Selector = require './rule/Selector' 2 | DeclarationBlock = require './rule/DeclarationBlock' 3 | 4 | module.exports = class Rule 5 | constructor: (selector) -> 6 | @selector = new Selector selector 7 | @styles = new DeclarationBlock 8 | 9 | setStyles: (styles) -> 10 | @styles.set styles 11 | this -------------------------------------------------------------------------------- /src/renderKid/styles/StyleSheet.coffee: -------------------------------------------------------------------------------- 1 | Rule = require './Rule' 2 | 3 | module.exports = class StyleSheet 4 | self = @ 5 | 6 | constructor: -> 7 | @_rulesBySelector = {} 8 | 9 | setRule: (selector, styles) -> 10 | if typeof selector is 'string' 11 | @_setRule selector, styles 12 | else if typeof selector is 'object' 13 | for key, val of selector 14 | @_setRule key, val 15 | 16 | this 17 | 18 | _setRule: (s, styles) -> 19 | for selector in self.splitSelectors s 20 | @_setSingleRule selector, styles 21 | 22 | this 23 | 24 | _setSingleRule: (s, styles) -> 25 | selector = self.normalizeSelector s 26 | unless rule = @_rulesBySelector[selector] 27 | rule = new Rule selector 28 | @_rulesBySelector[selector] = rule 29 | 30 | rule.setStyles styles 31 | this 32 | 33 | getRulesFor: (el) -> 34 | rules = [] 35 | for selector, rule of @_rulesBySelector 36 | if rule.selector.matches el then rules.push rule 37 | 38 | rules 39 | 40 | @normalizeSelector: (selector) -> 41 | selector 42 | .replace(/[\s]+/g, ' ') 43 | .replace(/[\s]*([>\,\+]{1})[\s]*/g, '$1') 44 | .trim() 45 | 46 | @splitSelectors: (s) -> 47 | s.trim().split ',' -------------------------------------------------------------------------------- /src/renderKid/styles/rule/DeclarationBlock.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class DeclarationBlock 2 | self = @ 3 | 4 | constructor: -> 5 | @_declarations = {} 6 | 7 | set: (prop, value) -> 8 | if typeof prop is 'object' 9 | for key, val of prop 10 | @set key, val 11 | 12 | return @ 13 | 14 | prop = self.sanitizeProp prop 15 | @_getDeclarationClass(prop).setOnto @_declarations, prop, value 16 | 17 | this 18 | 19 | _getDeclarationClass: (prop) -> 20 | if prop[0] is '_' 21 | return Arbitrary 22 | 23 | unless cls = declarationClasses[prop] 24 | throw Error "Unknown property `#{prop}`. Write it as `_#{prop}` if you're defining a custom property" 25 | 26 | return cls 27 | 28 | @sanitizeProp: (prop) -> 29 | String(prop).trim() 30 | 31 | Arbitrary = require './declarationBlock/Arbitrary' 32 | 33 | declarationClasses = 34 | color: require './declarationBlock/Color' 35 | background: require './declarationBlock/Background' 36 | 37 | width: require './declarationBlock/Width' 38 | height: require './declarationBlock/Height' 39 | 40 | bullet: require './declarationBlock/Bullet' 41 | display: require './declarationBlock/Display' 42 | 43 | margin: require './declarationBlock/Margin' 44 | marginTop: require './declarationBlock/MarginTop' 45 | marginLeft: require './declarationBlock/MarginLeft' 46 | marginRight: require './declarationBlock/MarginRight' 47 | marginBottom: require './declarationBlock/MarginBottom' 48 | 49 | padding: require './declarationBlock/Padding' 50 | paddingTop: require './declarationBlock/PaddingTop' 51 | paddingLeft: require './declarationBlock/PaddingLeft' 52 | paddingRight: require './declarationBlock/PaddingRight' 53 | paddingBottom: require './declarationBlock/PaddingBottom' -------------------------------------------------------------------------------- /src/renderKid/styles/rule/MixedDeclarationSet.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class MixedDeclarationSet 2 | self = @ 3 | 4 | @mix: (ruleSets...) -> 5 | mixed = new self 6 | for rules in ruleSets 7 | mixed.mixWithList rules 8 | 9 | mixed 10 | 11 | constructor: -> 12 | @_declarations = {} 13 | 14 | mixWithList: (rules) -> 15 | rules.sort (a, b) -> a.selector.priority > b.selector.priority 16 | for rule in rules 17 | @_mixWithRule rule 18 | 19 | this 20 | 21 | _mixWithRule: (rule) -> 22 | for prop, dec of rule.styles._declarations 23 | @_mixWithDeclaration dec 24 | 25 | return 26 | 27 | _mixWithDeclaration: (dec) -> 28 | cur = @_declarations[dec.prop] 29 | return if cur? and cur.important and not dec.important 30 | @_declarations[dec.prop] = dec 31 | return 32 | 33 | get: (prop) -> 34 | unless prop? 35 | return @_declarations 36 | 37 | return null unless @_declarations[prop]? 38 | @_declarations[prop].val 39 | 40 | toObject: -> 41 | obj = {} 42 | for prop, dec of @_declarations 43 | obj[prop] = dec.val 44 | 45 | obj -------------------------------------------------------------------------------- /src/renderKid/styles/rule/Selector.coffee: -------------------------------------------------------------------------------- 1 | CSSSelect = require 'css-select' 2 | 3 | module.exports = class Selector 4 | self = @ 5 | 6 | constructor: (@text) -> 7 | @_fn = CSSSelect.compile @text 8 | @priority = self.calculatePriority @text 9 | 10 | matches: (elem) -> 11 | CSSSelect.is elem, @_fn 12 | 13 | # This stupid piece of code is supposed to calculate 14 | # selector priority, somehow according to 15 | # http://www.w3.org/wiki/CSS/Training/Priority_level_of_selector 16 | @calculatePriority: (text) -> 17 | priotrity = 0 18 | 19 | if n = text.match /[\#]{1}/g 20 | priotrity += 100 * n.length 21 | 22 | if n = text.match /[a-zA-Z]+/g 23 | priotrity += 2 * n.length 24 | 25 | if n = text.match /\*/g 26 | priotrity += 1 * n.length 27 | 28 | priotrity -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Arbitrary.coffee: -------------------------------------------------------------------------------- 1 | _Declaration = require './_Declaration' 2 | 3 | module.exports = class Arbitrary extends _Declaration -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Background.coffee: -------------------------------------------------------------------------------- 1 | _Declaration = require './_Declaration' 2 | 3 | module.exports = class Background extends _Declaration -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Bullet.coffee: -------------------------------------------------------------------------------- 1 | _Declaration = require './_Declaration' 2 | 3 | module.exports = class Bullet extends _Declaration 4 | self = @ 5 | 6 | _set: (val) -> 7 | val = String val 8 | original = val 9 | char = null 10 | enabled = no 11 | color = 'none' 12 | bg = 'none' 13 | 14 | if m = val.match(/\"([^"]+)\"/) or m = val.match(/\'([^']+)\'/) 15 | char = m[1] 16 | val = val.replace m[0], '' 17 | enabled = yes 18 | 19 | if m = val.match(/(none|left|right|center)/) 20 | alignment = m[1] 21 | val = val.replace m[0], '' 22 | else 23 | alignment = 'left' 24 | 25 | if alignment is 'none' then enabled = no 26 | 27 | if m = val.match /color\:([\w\-]+)/ 28 | color = m[1] 29 | val = val.replace m[0], '' 30 | 31 | if m = val.match /bg\:([\w\-]+)/ 32 | bg = m[1] 33 | val = val.replace m[0], '' 34 | 35 | if val.trim() isnt '' 36 | throw Error "Unrecognizable value `#{original}` for `#{@prop}`" 37 | 38 | @val = 39 | enabled: enabled 40 | char: char 41 | alignment: alignment 42 | background: bg 43 | color: color -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Color.coffee: -------------------------------------------------------------------------------- 1 | _Declaration = require './_Declaration' 2 | 3 | module.exports = class Color extends _Declaration -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Display.coffee: -------------------------------------------------------------------------------- 1 | _Declaration = require './_Declaration' 2 | 3 | module.exports = class Display extends _Declaration 4 | self = @ 5 | @_allowed: ['inline', 'block', 'none'] 6 | 7 | _set: (val) -> 8 | val = String(val).toLowerCase() 9 | unless val in self._allowed 10 | throw Error "Unrecognizable value `#{val}` for `#{@prop}`" 11 | 12 | @val = val -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Height.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class Height extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Margin.coffee: -------------------------------------------------------------------------------- 1 | _Declaration = require './_Declaration' 2 | 3 | MarginTop = require './MarginTop' 4 | MarginLeft = require './MarginLeft' 5 | MarginRight = require './MarginRight' 6 | MarginBottom = require './MarginBottom' 7 | 8 | module.exports = class Margin extends _Declaration 9 | self = @ 10 | 11 | @setOnto: (declarations, prop, originalValue) -> 12 | append = '' 13 | val = _Declaration.sanitizeValue originalValue 14 | 15 | if _Declaration.importantClauseRx.test String(val) 16 | append = ' !important' 17 | val = val.replace _Declaration.importantClauseRx, '' 18 | 19 | val = val.trim() 20 | if val.length is 0 21 | return self._setAllDirections declarations, append, append, append, append 22 | 23 | vals = val.split(" ").map (val) -> val + append 24 | if vals.length is 1 25 | self._setAllDirections declarations, vals[0], vals[0], vals[0], vals[0] 26 | else if vals.length is 2 27 | self._setAllDirections declarations, vals[0], vals[1], vals[0], vals[1] 28 | else if vals.length is 3 29 | self._setAllDirections declarations, vals[0], vals[1], vals[2], vals[1] 30 | else if vals.length is 4 31 | self._setAllDirections declarations, vals[0], vals[1], vals[2], vals[3] 32 | else 33 | throw Error "Can't understand value for margin: `#{originalValue}`" 34 | 35 | @_setAllDirections: (declarations, top, right, bottom, left) -> 36 | MarginTop.setOnto declarations, 'marginTop', top 37 | MarginTop.setOnto declarations, 'marginRight', right 38 | MarginTop.setOnto declarations, 'marginBottom', bottom 39 | MarginTop.setOnto declarations, 'marginLeft', left 40 | 41 | return -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/MarginBottom.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class MarginBottom extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/MarginLeft.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class MarginLeft extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/MarginRight.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class MarginRight extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/MarginTop.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class MarginTop extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Padding.coffee: -------------------------------------------------------------------------------- 1 | _Declaration = require './_Declaration' 2 | PaddingTop = require './PaddingTop' 3 | PaddingLeft = require './PaddingLeft' 4 | PaddingRight = require './PaddingRight' 5 | PaddingBottom = require './PaddingBottom' 6 | 7 | module.exports = class Padding extends _Declaration 8 | self = @ 9 | 10 | @setOnto: (declarations, prop, originalValue) -> 11 | append = '' 12 | val = _Declaration.sanitizeValue originalValue 13 | if _Declaration.importantClauseRx.test String(val) 14 | append = ' !important' 15 | val = val.replace _Declaration.importantClauseRx, '' 16 | 17 | val = val.trim() 18 | if val.length is 0 19 | return self._setAllDirections declarations, append, append, append, append 20 | 21 | vals = val.split(" ").map (val) -> val + append 22 | if vals.length is 1 23 | self._setAllDirections declarations, vals[0], vals[0], vals[0], vals[0] 24 | else if vals.length is 2 25 | self._setAllDirections declarations, vals[0], vals[1], vals[0], vals[1] 26 | else if vals.length is 3 27 | self._setAllDirections declarations, vals[0], vals[1], vals[2], vals[1] 28 | else if vals.length is 4 29 | self._setAllDirections declarations, vals[0], vals[1], vals[2], vals[3] 30 | else 31 | throw Error "Can't understand value for padding: `#{originalValue}`" 32 | 33 | @_setAllDirections: (declarations, top, right, bottom, left) -> 34 | PaddingTop.setOnto declarations, 'paddingTop', top 35 | PaddingTop.setOnto declarations, 'paddingRight', right 36 | PaddingTop.setOnto declarations, 'paddingBottom', bottom 37 | PaddingTop.setOnto declarations, 'paddingLeft', left 38 | 39 | return -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/PaddingBottom.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class PaddingBottom extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/PaddingLeft.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class PaddingLeft extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/PaddingRight.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class PaddingRight extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/PaddingTop.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class PaddingTop extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/Width.coffee: -------------------------------------------------------------------------------- 1 | _Length = require './_Length' 2 | 3 | module.exports = class Width extends _Length -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/_Declaration.coffee: -------------------------------------------------------------------------------- 1 | # Abstract Style Declaration 2 | module.exports = class _Declaration 3 | self = @ 4 | @importantClauseRx: /(\s\!important)$/ 5 | 6 | @setOnto: (declarations, prop, val) -> 7 | unless dec = declarations[prop] 8 | declarations[prop] = new @ prop, val 9 | else 10 | dec.set val 11 | 12 | @sanitizeValue: (val) -> 13 | String(val).trim().replace /[\s]+/g, ' ' 14 | 15 | @inheritAllowed: no 16 | 17 | constructor: (@prop, val) -> 18 | @important = no 19 | @set val 20 | 21 | get: -> 22 | @_get() 23 | 24 | _get: -> 25 | @val 26 | 27 | _pickImportantClause: (val) -> 28 | if self.importantClauseRx.test String(val) 29 | @important = yes 30 | val.replace self.importantClauseRx, '' 31 | else 32 | @important = no 33 | val 34 | 35 | set: (val) -> 36 | val = self.sanitizeValue val 37 | val = @_pickImportantClause val 38 | val = val.trim() 39 | 40 | return @ if @_handleNullOrInherit val 41 | @_set val 42 | this 43 | 44 | _set: (val) -> 45 | @val = val 46 | 47 | _handleNullOrInherit: (val) -> 48 | if val is '' 49 | @val = '' 50 | return true 51 | 52 | if val is 'inherit' 53 | if @constructor.inheritAllowed 54 | @val = 'inherit' 55 | else 56 | throw Error "Inherit is not allowed for `#{@prop}`" 57 | 58 | true 59 | else 60 | false -------------------------------------------------------------------------------- /src/renderKid/styles/rule/declarationBlock/_Length.coffee: -------------------------------------------------------------------------------- 1 | _Declaration = require './_Declaration' 2 | 3 | module.exports = class _Length extends _Declaration 4 | _set: (val) -> 5 | unless /^[0-9]+$/.test String(val) 6 | throw Error "`#{@prop}` only takes an integer for value" 7 | 8 | @val = parseInt val -------------------------------------------------------------------------------- /src/tools.coffee: -------------------------------------------------------------------------------- 1 | htmlparser = require 'htmlparser2' 2 | {objectToDom} = require 'dom-converter' 3 | merge = require 'lodash/merge' 4 | cloneDeep = require 'lodash/cloneDeep' 5 | isPlainObject = require 'lodash/isPlainObject' 6 | 7 | module.exports = self = 8 | repeatString: (str, times) -> 9 | output = '' 10 | for i in [0...times] 11 | output += str 12 | 13 | output 14 | 15 | cloneAndMergeDeep: (base, toAppend) -> 16 | merge cloneDeep(base), toAppend 17 | 18 | toDom: (subject) -> 19 | if typeof subject is 'string' 20 | self.stringToDom subject 21 | else if isPlainObject subject 22 | self._objectToDom subject 23 | else 24 | throw Error "tools.toDom() only supports strings and objects" 25 | 26 | stringToDom: (string) -> 27 | handler = new htmlparser.DomHandler 28 | parser = new htmlparser.Parser handler 29 | parser.write string 30 | parser.end() 31 | handler.dom 32 | 33 | _fixQuotesInDom: (input) -> 34 | if Array.isArray input 35 | for node in input 36 | self._fixQuotesInDom node 37 | return input 38 | 39 | node = input 40 | if node.type is 'text' 41 | node.data = self._quoteNodeText node.data 42 | else 43 | self._fixQuotesInDom node.children 44 | 45 | objectToDom: (o) -> 46 | unless Array.isArray(o) 47 | unless isPlainObject(o) 48 | throw Error "objectToDom() only accepts a bare object or an array" 49 | 50 | self._fixQuotesInDom objectToDom o 51 | 52 | quote: (str) -> 53 | String(str) 54 | .replace(//g, '>') 56 | .replace(/\"/g, '"') 57 | .replace(/\ /g, '&sp;') 58 | .replace(/\n/g, '
    ') 59 | 60 | _quoteNodeText: (text) -> 61 | String(text) 62 | .replace(/\&/g, '&') 63 | .replace(//g, '>') 65 | .replace(/\"/g, '"') 66 | .replace(/\ /g, '&sp;') 67 | .replace(/\n/g, "&nl;") 68 | 69 | getCols: -> 70 | # Based on https://github.com/jonschlinkert/window-size 71 | tty = require 'tty' 72 | 73 | cols = 74 | try 75 | if tty.isatty(1) && tty.isatty(2) 76 | if process.stdout.getWindowSize 77 | process.stdout.getWindowSize(1)[0] 78 | else if tty.getWindowSize 79 | tty.getWindowSize()[1] 80 | else if process.stdout.columns 81 | process.stdout.columns 82 | 83 | if typeof cols is 'number' && cols > 30 then cols else 80 -------------------------------------------------------------------------------- /test/AnsiPainter.coffee: -------------------------------------------------------------------------------- 1 | AnsiPainter = require '../src/AnsiPainter' 2 | 3 | paint = (t) -> 4 | AnsiPainter.paint(t) 5 | 6 | describe "AnsiPainter", -> 7 | describe "paint()", -> 8 | it "should handle basic coloring", -> 9 | t = "a" 10 | paint(t).should.equal '\u001b[30m\u001b[47ma\u001b[0m' 11 | 12 | it "should handle color in color", -> 13 | t = "ab" 14 | paint(t).should.equal '\u001b[31ma\u001b[0m\u001b[34mb\u001b[0m' 15 | 16 | it "should skip empty tags", -> 17 | t = "a" 18 | paint(t).should.equal 'a\u001b[0m' 19 | 20 | describe "_replaceSpecialStrings()", -> 21 | it "should work", -> 22 | AnsiPainter::_replaceSpecialStrings('<>"&sp;&').should.equal '<>" &' -------------------------------------------------------------------------------- /test/Layout.coffee: -------------------------------------------------------------------------------- 1 | Layout = require '../src/Layout' 2 | 3 | describe "Layout", -> 4 | describe "constructor()", -> 5 | it "should create root block", -> 6 | l = new Layout 7 | expect(l._root).to.exist 8 | l._root._name.should.equal '__root' 9 | 10 | describe "get()", -> 11 | it "should not be allowed when any block is open", -> 12 | l = new Layout 13 | l.openBlock() 14 | (-> 15 | l.get() 16 | ).should.throw Error -------------------------------------------------------------------------------- /test/RenderKid.coffee: -------------------------------------------------------------------------------- 1 | RenderKid = require '../src/RenderKid' 2 | {strip} = require '../src/AnsiPainter' 3 | 4 | match = (input, expected, setStuff) -> 5 | r = new RenderKid 6 | r.style 7 | span: 8 | display: 'inline' 9 | div: 10 | display: 'block' 11 | 12 | setStuff?(r) 13 | strip(r.render(input)).trim().should.equal expected.trim() 14 | 15 | describe "RenderKid", -> 16 | describe "constructor()", -> 17 | it "should work", -> 18 | new RenderKid 19 | 20 | describe "whitespace management - inline", -> 21 | it "shouldn't put extra whitespaces", -> 22 | input = """ 23 | 24 | abc 25 | 26 | """ 27 | 28 | expected = """ 29 | 30 | abc 31 | 32 | """ 33 | 34 | match input, expected 35 | 36 | it "should allow 1 whitespace character on each side", -> 37 | input = """ 38 | 39 | a b c 40 | 41 | """ 42 | 43 | expected = """ 44 | 45 | a b c 46 | 47 | """ 48 | 49 | match input, expected 50 | 51 | it "should eliminate extra whitespaces inside text", -> 52 | input = """ 53 | 54 | ab1 \n b2c 55 | 56 | """ 57 | 58 | expected = """ 59 | 60 | ab1 b2c 61 | 62 | """ 63 | 64 | match input, expected 65 | 66 | it "should allow line breaks with
    ", -> 67 | input = """ 68 | 69 | ab1
    b2
    c 70 | 71 | """ 72 | 73 | expected = """ 74 | 75 | ab1\nb2c 76 | 77 | """ 78 | 79 | match input, expected 80 | 81 | it "should allow line breaks with &nl;", -> 82 | input = """ 83 | 84 | ab1&nl;b2c 85 | 86 | """ 87 | 88 | expected = """ 89 | 90 | ab1\nb2c 91 | 92 | """ 93 | 94 | match input, expected 95 | 96 | it "should allow whitespaces with &sp;", -> 97 | input = """ 98 | 99 | ab1&sp;b2c 100 | 101 | """ 102 | 103 | expected = """ 104 | 105 | ab1 b2c 106 | 107 | """ 108 | 109 | match input, expected 110 | 111 | describe "whitespace management - block", -> 112 | it "should add one linebreak between two blocks", -> 113 | input = """ 114 | 115 |
    a
    116 |
    b
    117 | 118 | """ 119 | 120 | expected = """ 121 | 122 | a 123 | b 124 | 125 | """ 126 | 127 | match input, expected 128 | 129 | it "should ignore empty blocks", -> 130 | input = """ 131 | 132 |
    a
    133 |
    134 |
    b
    135 | 136 | """ 137 | 138 | expected = """ 139 | 140 | a 141 | b 142 | 143 | """ 144 | 145 | match input, expected 146 | 147 | it "should add an extra linebreak between two adjacent blocks inside an inline", -> 148 | input = """ 149 | 150 | 151 |
    a
    152 |
    b
    153 |
    154 | 155 | """ 156 | 157 | expected = """ 158 | 159 | a 160 | 161 | b 162 | 163 | """ 164 | 165 | match input, expected 166 | 167 | it "example: div(marginBottom:1)+div", -> 168 | input = """ 169 | 170 |
    a
    171 |
    b
    172 | 173 | """ 174 | 175 | expected = """ 176 | 177 | a 178 | 179 | b 180 | 181 | """ 182 | 183 | match input, expected, (r) -> 184 | r.style '.first': marginBottom: 1 185 | 186 | it "example: div+div(marginTop:1)", -> 187 | input = """ 188 | 189 |
    a
    190 |
    b
    191 | 192 | """ 193 | 194 | expected = """ 195 | 196 | a 197 | 198 | b 199 | 200 | """ 201 | 202 | match input, expected, (r) -> 203 | r.style '.second': marginTop: 1 204 | 205 | it "example: div(marginBottom:1)+div(marginTop:1)", -> 206 | input = """ 207 | 208 |
    a
    209 |
    b
    210 | 211 | """ 212 | 213 | expected = """ 214 | 215 | a 216 | 217 | 218 | b 219 | 220 | """ 221 | 222 | match input, expected, (r) -> 223 | r.style 224 | '.first': marginBottom: 1 225 | '.second': marginTop: 1 226 | 227 | it "example: div(marginBottom:2)+div(marginTop:1)", -> 228 | input = """ 229 | 230 |
    a
    231 |
    b
    232 | 233 | """ 234 | 235 | expected = """ 236 | 237 | a 238 | 239 | 240 | 241 | b 242 | 243 | """ 244 | 245 | match input, expected, (r) -> 246 | r.style 247 | '.first': marginBottom: 2 248 | '.second': marginTop: 1 249 | 250 | it "example: div(marginBottom:2)+span+div(marginTop:1)", -> 251 | input = """ 252 | 253 |
    a
    254 | span 255 |
    b
    256 | 257 | """ 258 | 259 | expected = """ 260 | 261 | a 262 | 263 | 264 | span 265 | 266 | b 267 | 268 | """ 269 | 270 | match input, expected, (r) -> 271 | r.style 272 | '.first': marginBottom: 2 273 | '.second': marginTop: 1 -------------------------------------------------------------------------------- /test/layout/Block.coffee: -------------------------------------------------------------------------------- 1 | Layout = require '../../src/Layout' 2 | merge = require 'lodash/merge' 3 | {cloneAndMergeDeep} = require '../../src/tools' 4 | 5 | {open, get, conf} = do -> 6 | show = (layout) -> 7 | got = layout.get() 8 | got = got.replace /<[^>]+>/g, '' 9 | 10 | defaultBlockConfig = 11 | linePrependor: options: amount: 2 12 | 13 | c = (add = {}) -> 14 | cloneAndMergeDeep defaultBlockConfig, add 15 | 16 | ret = {} 17 | 18 | ret.open = (block, name, top = 0, bottom = 0) -> 19 | config = c 20 | blockPrependor: options: amount: top 21 | blockAppendor: options: amount: bottom 22 | 23 | b = block.openBlock config, name 24 | b.write name + ' | top ' + top + ' bottom ' + bottom 25 | b 26 | 27 | ret.get = (layout) -> 28 | layout.get().replace(/<[^>]+>/g, '') 29 | 30 | ret.conf = (props) -> 31 | config = {} 32 | if props.left? 33 | merge config, linePrependor: options: amount: props.left 34 | 35 | if props.right? 36 | merge config, lineAppendor: options: amount: props.right 37 | 38 | if props.top? 39 | merge config, blockPrependor: options: amount: props.top 40 | 41 | if props.bottom? 42 | merge config, blockAppendor: options: amount: props.bottom 43 | 44 | if props.width? 45 | merge config, width: props.width 46 | 47 | if props.bullet is yes 48 | merge config, linePrependor: options: bullet: {char: '-', alignment: 'left'} 49 | 50 | config 51 | 52 | ret 53 | 54 | 55 | describe "Layout", -> 56 | describe "inline inputs", -> 57 | it "should be merged", -> 58 | l = new Layout 59 | 60 | l.write 'a' 61 | l.write 'b' 62 | 63 | get(l).should.equal 'ab' 64 | 65 | it "should be correctly wrapped", -> 66 | l = new Layout 67 | block = l.openBlock conf width: 20 68 | block.write '123456789012345678901234567890' 69 | block.close() 70 | get(l).should.equal '12345678901234567890\n1234567890' 71 | 72 | it "should trim from left when wrapping to a new line", -> 73 | l = new Layout 74 | block = l.openBlock conf width: 20 75 | block.write '12345678901234567890 \t 123456789012345678901' 76 | block.close() 77 | get(l).should.equal '12345678901234567890\n12345678901234567890\n1' 78 | 79 | it "should handle line breaks correctly", -> 80 | l = new Layout 81 | block = l.openBlock conf width: 20 82 | block.write '\na\n\nb\n' 83 | block.close() 84 | get(l).should.equal '\na\n\nb\n' 85 | 86 | it "should not put extra line breaks when a line is already broken", -> 87 | l = new Layout 88 | block = l.openBlock conf width: 20 89 | block.write '01234567890123456789\n0123456789' 90 | block.close() 91 | get(l).should.equal '01234567890123456789\n0123456789' 92 | 93 | describe "horizontal margins", -> 94 | it "should account for left margins", -> 95 | l = new Layout 96 | block = l.openBlock conf width: 20, left: 2 97 | block.write '01' 98 | block.close() 99 | get(l).should.equal ' 01' 100 | 101 | it "should account for right margins", -> 102 | l = new Layout 103 | block = l.openBlock conf width: 20, right: 2 104 | block.write '01' 105 | block.close() 106 | get(l).should.equal '01 ' 107 | 108 | it "should account for both margins", -> 109 | l = new Layout 110 | block = l.openBlock conf width: 20, right: 2, left: 1 111 | block.write '01' 112 | block.close() 113 | get(l).should.equal ' 01 ' 114 | 115 | it "should break lines according to left margins", -> 116 | l = new Layout 117 | global.tick = yes 118 | block = l.openBlock conf width: 20, left: 2 119 | block.write '01234567890123456789' 120 | block.close() 121 | global.tick = no 122 | get(l).should.equal ' 01234567890123456789' 123 | 124 | it "should break lines according to right margins", -> 125 | l = new Layout 126 | block = l.openBlock conf width: 20, right: 2 127 | block.write '01234567890123456789' 128 | block.close() 129 | get(l).should.equal '01234567890123456789 ' 130 | 131 | it "should break lines according to both margins", -> 132 | l = new Layout 133 | block = l.openBlock conf width: 20, right: 2, left: 1 134 | block.write '01234567890123456789' 135 | block.close() 136 | get(l).should.equal ' 01234567890123456789 ' 137 | 138 | it "should break lines according to terminal width", -> 139 | l = new Layout terminalWidth: 20 140 | block = l.openBlock conf right: 2, left: 1 141 | block.write '01234567890123456789' 142 | block.close() 143 | 144 | # Note: We don't expect ' 01234567890123456 \n 789 ', 145 | # since the first line (' 01234567890123456 ') is a full line 146 | # according to layout.config.terminalWidth and doesn't need 147 | # a break line. 148 | get(l).should.equal ' 01234567890123456 789 ' 149 | 150 | describe "lines and blocks", -> 151 | it "should put one break line between: line, block", -> 152 | l = new Layout 153 | l.write 'a' 154 | l.openBlock().write('b').close() 155 | get(l).should.equal 'a\nb' 156 | 157 | it "should put one break line between: block, line", -> 158 | l = new Layout 159 | l.openBlock().write('a').close() 160 | l.write 'b' 161 | get(l).should.equal 'a\nb' 162 | 163 | it "should put one break line between: line, block, line", -> 164 | l = new Layout 165 | l.write 'a' 166 | l.openBlock().write('b').close() 167 | l.write 'c' 168 | get(l).should.equal 'a\nb\nc' 169 | 170 | it "margin top should work for: line, block", -> 171 | l = new Layout 172 | l.write 'a' 173 | l.openBlock(conf top: 2).write('b').close() 174 | get(l).should.equal 'a\n\n\nb' 175 | 176 | it "margin top should work for: block, line", -> 177 | l = new Layout 178 | l.openBlock(conf top: 1).write('a').close() 179 | l.write 'b' 180 | get(l).should.equal '\na\nb' 181 | 182 | it "margin top should work for: block, line, when block starts with a break", -> 183 | l = new Layout 184 | l.openBlock(conf top: 1).write('\na').close() 185 | l.write 'b' 186 | get(l).should.equal '\n\na\nb' 187 | 188 | it "margin top should work for: line, block, when line ends with a break", -> 189 | l = new Layout 190 | l.write 'a\n' 191 | l.openBlock(conf top: 1).write('b').close() 192 | get(l).should.equal 'a\n\n\nb' 193 | 194 | it "margin top should work for: line, block, when there are two breaks in between", -> 195 | l = new Layout 196 | l.write 'a\n' 197 | l.openBlock(conf top: 1).write('\nb').close() 198 | get(l).should.equal 'a\n\n\n\nb' 199 | 200 | it "margin bottom should work for: line, block", -> 201 | l = new Layout 202 | l.write 'a' 203 | l.openBlock(conf bottom: 1).write('b').close() 204 | get(l).should.equal 'a\nb\n' 205 | 206 | it "margin bottom should work for: block, line", -> 207 | l = new Layout 208 | l.openBlock(conf bottom: 1).write('a').close() 209 | l.write 'b' 210 | get(l).should.equal 'a\n\nb' 211 | 212 | it "margin bottom should work for: block, line, when block ends with a break", -> 213 | l = new Layout 214 | l.openBlock(conf bottom: 1).write('a\n').close() 215 | l.write 'b' 216 | get(l).should.equal 'a\n\n\nb' 217 | 218 | it "margin bottom should work for: block, line, when line starts with a break", -> 219 | l = new Layout 220 | l.openBlock(conf bottom: 1).write('a').close() 221 | l.write '\nb' 222 | get(l).should.equal 'a\n\n\nb' 223 | 224 | it "margin bottom should work for: block, line, when there are two breaks in between", -> 225 | l = new Layout 226 | l.openBlock(conf bottom: 1).write('a\n').close() 227 | l.write '\nb' 228 | get(l).should.equal 'a\n\n\n\nb' 229 | 230 | describe "blocks and blocks", -> 231 | it "should not get extra break lines for full-width lines", -> 232 | l = new Layout 233 | l.openBlock(conf width: 20).write('01234567890123456789').close() 234 | l.openBlock().write('b').close() 235 | get(l).should.equal '01234567890123456789\nb' 236 | 237 | it "should not get extra break lines for full-width lines followed by a margin", -> 238 | l = new Layout 239 | l.openBlock(conf width: 20, bottom: 1).write('01234567890123456789').close() 240 | l.openBlock().write('b').close() 241 | get(l).should.equal '01234567890123456789\n\nb' 242 | 243 | it "a(top: 0, bottom: 0) b(top: 0, bottom: 0)", -> 244 | l = new Layout 245 | l.openBlock().write('a').close() 246 | l.openBlock().write('b').close() 247 | get(l).should.equal 'a\nb' 248 | 249 | it "a(top: 0, bottom: 0) b(top: 1, bottom: 0)", -> 250 | l = new Layout 251 | l.openBlock().write('a').close() 252 | l.openBlock(conf(top: 1)).write('b').close() 253 | get(l).should.equal 'a\n\nb' 254 | 255 | it "a(top: 0, bottom: 1) b(top: 0, bottom: 0)", -> 256 | l = new Layout 257 | l.openBlock(conf(bottom: 1)).write('a').close() 258 | l.openBlock().write('b').close() 259 | get(l).should.equal 'a\n\nb' 260 | 261 | it "a(top: 0, bottom: 1 ) b( top: 1, bottom: 0)", -> 262 | l = new Layout 263 | l.openBlock(conf(bottom: 1)).write('a').close() 264 | l.openBlock(conf(top: 1)).write('b').close() 265 | get(l).should.equal 'a\n\n\nb' 266 | 267 | it "a(top: 0, bottom: 1 br) b(br top: 1, bottom: 0)", -> 268 | l = new Layout 269 | l.openBlock(conf(bottom: 1)).write('a\n').close() 270 | l.openBlock(conf(top: 1)).write('\nb').close() 271 | get(l).should.equal 'a\n\n\n\n\nb' 272 | 273 | it "a(top: 2, bottom: 3 a1-br-a2) b(br-b1-br-br-b2-br top: 2, bottom: 3)", -> 274 | l = new Layout 275 | l.openBlock(conf(top: 2, bottom: 3)).write('a1\na2').close() 276 | l.openBlock(conf(top: 2, bottom: 3)).write('\nb1\n\nb2\n').close() 277 | get(l).should.equal '\n\na1\na2\n\n\n\n\n\n\nb1\n\nb2\n\n\n\n' 278 | 279 | describe "nesting", -> 280 | it "should break one line for nested blocks", -> 281 | l = new Layout 282 | l.write 'a' 283 | b = l.openBlock() 284 | c = b.openBlock().write('c').close() 285 | b.close() 286 | get(l).should.equal 'a\nc' 287 | 288 | it "a(left: 2) > b(top: 2)", -> 289 | l = new Layout 290 | a = l.openBlock(conf(left: 2)) 291 | a.openBlock(conf(top: 2)).write('b').close() 292 | a.close() 293 | get(l).should.equal ' \n \n b' 294 | 295 | it "a(left: 2) > b(bottom: 2)", -> 296 | l = new Layout 297 | a = l.openBlock(conf(left: 2)) 298 | a.openBlock(conf(bottom: 2)).write('b').close() 299 | a.close() 300 | get(l).should.equal ' b\n \n ' 301 | 302 | describe "bullets", -> 303 | it "basic bullet", -> 304 | l = new Layout 305 | l.openBlock(conf(left: 3, bullet: yes)).write('a').close() 306 | get(l).should.equal '- a' 307 | 308 | it "a(left: 3, bullet) > b(top:1)", -> 309 | l = new Layout 310 | a = l.openBlock(conf(left: 3, bullet: yes)) 311 | b = a.openBlock(conf(top: 1)).write('b').close() 312 | a.close() 313 | get(l).should.equal '- \n b' -------------------------------------------------------------------------------- /test/layout/SpecialString.coffee: -------------------------------------------------------------------------------- 1 | S = require '../../src/layout/SpecialString' 2 | 3 | describe "SpecialString", -> 4 | describe 'SpecialString()', -> 5 | it 'should return instance', -> 6 | new S('s').should.be.instanceOf S 7 | 8 | describe 'length()', -> 9 | it 'should return correct length for normal text', -> 10 | new S('hello').length.should.equal 5 11 | 12 | it 'should return correct length for text containing tabs and tags', -> 13 | new S('hel\tlo').length.should.equal 13 14 | 15 | it "shouldn't count empty tags as tags", -> 16 | new S('<>><').length.should.equal 4 17 | 18 | it "should count length of single tag as 0", -> 19 | new S('').length.should.equal 0 20 | 21 | it "should work correctly with html quoted characters", -> 22 | new S(' >< &sp;').length.should.equal 5 23 | 24 | describe 'splitIn()', -> 25 | it "should work correctly with normal text", -> 26 | new S("123456").splitIn(3).should.be.like ['123', '456'] 27 | 28 | it "should work correctly with normal text containing tabs and tags", -> 29 | new S("12\t3456").splitIn(3).should.be.like ['12', '\t', '345', '6'] 30 | 31 | it "should not trimLeft all lines when trimLeft is no", -> 32 | new S('abc def').splitIn(3).should.be.like ['abc', ' de', 'f'] 33 | 34 | it "should trimLeft all lines when trimLeft is true", -> 35 | new S('abc def').splitIn(3, yes).should.be.like ['abc', 'def'] 36 | 37 | describe 'cut()', -> 38 | it "should work correctly with text containing tabs and tags", -> 39 | original = new S("12\t3456") 40 | cut = original.cut(2, 3) 41 | original.str.should.equal '123456' 42 | cut.str.should.equal '\t' 43 | 44 | it "should trim left when trimLeft is true", -> 45 | original = new S ' 132' 46 | cut = original.cut 0, 1, yes 47 | original.str.should.equal '32' 48 | cut.str.should.equal '1' 49 | 50 | it "should be greedy", -> 51 | new S("aba").cut(0, 2).str.should.equal "ab" 52 | 53 | describe 'isOnlySpecialChars()', -> 54 | it "should work", -> 55 | new S("12\t3456").isOnlySpecialChars().should.equal no 56 | new S("").isOnlySpecialChars().should.equal yes 57 | 58 | describe 'clone()', -> 59 | it "should return independent instance", -> 60 | a = new S('hello') 61 | b = a.clone() 62 | a.str.should.equal b.str 63 | a.should.not.equal b 64 | 65 | describe 'trim()', -> 66 | it "should return an independent instance", -> 67 | s = new S('') 68 | s.trim().should.not.equal s 69 | 70 | it 'should return the same string when trim is not required', -> 71 | new S('hello').trim().str.should.equal 'hello' 72 | 73 | it 'should return trimmed string', -> 74 | new S(' hello').trim().str.should.equal 'hello' 75 | -------------------------------------------------------------------------------- /test/mochaHelpers.coffee: -------------------------------------------------------------------------------- 1 | chai = require('chai') 2 | 3 | chai 4 | .use(require 'chai-fuzzy') 5 | .use(require 'chai-changes') 6 | .use(require 'sinon-chai') 7 | .should() 8 | 9 | global.expect = chai.expect 10 | global.sinon = require 'sinon' -------------------------------------------------------------------------------- /test/renderKid/styles/StyleSheet.coffee: -------------------------------------------------------------------------------- 1 | StyleSheet = require '../../../src/renderKid/styles/StyleSheet' 2 | 3 | describe "StyleSheet", -> 4 | describe "normalizeSelector()", -> 5 | it 'should remove unnecessary spaces', -> 6 | StyleSheet.normalizeSelector(' body+a s > a ') 7 | .should.equal 'body+a s>a' -------------------------------------------------------------------------------- /test/tools.coffee: -------------------------------------------------------------------------------- 1 | tools = require '../src/tools' 2 | 3 | describe "tools", -> 4 | describe "quote()", -> 5 | it "should convert html special strings to their entities", -> 6 | tools.quote(" abc<>\"\n") 7 | .should.equal '&sp;abc<>"
    ' 8 | 9 | describe "stringToDom()", -> 10 | it "should work", -> 11 | tools.stringToDom(' texttext text texttexttexttext') 12 | 13 | describe "objectToDom()", -> 14 | it "should work", -> 15 | tools.objectToDom({a: 'text'}) 16 | 17 | it "should have quoted text nodes", -> 18 | tools.objectToDom({a: '&<> "'})[0].children[0] 19 | .data.should.equal '&<>&sp;"' --------------------------------------------------------------------------------