├── .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 | [](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 |
43 | -
44 | Name:
45 | RenderKid
46 |
47 | -
48 | Version:
49 | 0.2
50 |
51 | -
52 | Last Update:
53 | Jan 2015
54 |
55 |
56 | ")
57 |
58 | console.log(output)
59 | ```
60 |
61 | 
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 | 
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 |
121 | - Item 1
122 | - Item 2
123 | - Item 3
124 |
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 | 
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 + "#{tag}>"
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
b2c
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;"'
--------------------------------------------------------------------------------