Comparing version 2.0.0-6 to 2.0.0-7
@@ -8,2 +8,4 @@ "use strict"; | ||
var _dom = require("./dom"); | ||
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } | ||
@@ -15,6 +17,6 @@ | ||
if (element.nodeValue) { | ||
return document.createTextNode(element.nodeValue); | ||
return (0, _dom.createTextNode)(element.nodeValue); | ||
} | ||
const newElement = document.createElement(element.nodeName); | ||
const newElement = (0, _dom.createElement)(element.nodeName); | ||
newElement.style = _objectSpread({}, element.style); | ||
@@ -28,3 +30,3 @@ newElement.static = element.static; | ||
for (const childNode of element.childNodes) { | ||
newElement.appendChild(cloneElement(document, childNode)); | ||
(0, _dom.appendChild)(newElement, cloneElement(document, childNode)); | ||
} | ||
@@ -31,0 +33,0 @@ |
@@ -77,3 +77,3 @@ "use strict"; | ||
// eslint-disable-line camelcase | ||
children: _propTypes.default.node.isRequired | ||
children: _propTypes.default.node | ||
}); | ||
@@ -80,0 +80,0 @@ |
@@ -10,5 +10,7 @@ "use strict"; | ||
var _dom = require("./dom"); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var _default = (document, onRender) => { | ||
var _default = onRender => { | ||
const rootHostContext = {}; | ||
@@ -26,3 +28,3 @@ const childHostContext = {}; | ||
createInstance: (type, newProps) => { | ||
const element = document.createElement(type); | ||
const node = (0, _dom.createNode)(type); | ||
@@ -33,29 +35,29 @@ for (const [key, value] of Object.entries(newProps)) { | ||
// Text node must be wrapped in another node, so that text can be aligned within container | ||
const textElement = document.createElement('span'); | ||
const textElement = (0, _dom.createNode)('span'); | ||
textElement.textContent = value; | ||
element.appendChild(textElement); | ||
(0, _dom.appendChildNode)(node, textElement); | ||
} | ||
} else if (key === 'style') { | ||
Object.assign(element.style, value); | ||
Object.assign(node.style, value); | ||
} else if (key === 'unstable__transformChildren') { | ||
element.unstable__transformChildren = value; // eslint-disable-line camelcase | ||
node.unstable__transformChildren = value; // eslint-disable-line camelcase | ||
} else if (key === 'static') { | ||
element.static = true; | ||
node.static = true; | ||
} else { | ||
element.setAttribute(key, value); | ||
(0, _dom.setAttribute)(node, key, value); | ||
} | ||
} | ||
return element; | ||
return node; | ||
}, | ||
createTextInstance: text => document.createTextNode(text), | ||
resetTextContent: element => { | ||
if (element.textContent) { | ||
element.textContent = ''; | ||
createTextInstance: _dom.createTextNode, | ||
resetTextContent: node => { | ||
if (node.textContent) { | ||
node.textContent = ''; | ||
} | ||
if (element.childNodes.length > 0) { | ||
for (const childNode of element.childNodes) { | ||
if (node.childNodes.length > 0) { | ||
for (const childNode of node.childNodes) { | ||
childNode.yogaNode.free(); | ||
element.removeChild(childNode); | ||
(0, _dom.removeChildNode)(node, childNode); | ||
} | ||
@@ -65,30 +67,30 @@ } | ||
getPublicInstance: instance => instance, | ||
appendInitialChild: (parent, child) => parent.appendChild(child), | ||
appendChild: (parent, child) => parent.appendChild(child), | ||
insertBefore: (parent, child, beforeChild) => parent.insertBefore(child, beforeChild), | ||
appendInitialChild: _dom.appendChildNode, | ||
appendChild: _dom.appendChildNode, | ||
insertBefore: _dom.insertBeforeNode, | ||
finalizeInitialChildren: () => {}, | ||
supportsMutation: true, | ||
appendChildToContainer: (parent, child) => { | ||
parent.appendChild(child); | ||
appendChildToContainer: (parentNode, childNode) => { | ||
(0, _dom.appendChildNode)(parentNode, childNode); | ||
onRender(); | ||
}, | ||
removeChildFromContainer: (parent, child) => { | ||
parent.removeChild(child); | ||
removeChildFromContainer: (parentNode, childNode) => { | ||
(0, _dom.removeChildNode)(parentNode, childNode); | ||
onRender(); | ||
}, | ||
prepareUpdate: () => true, | ||
commitUpdate: (element, updatePayload, type, oldProps, newProps) => { | ||
commitUpdate: (node, updatePayload, type, oldProps, newProps) => { | ||
for (const [key, value] of Object.entries(newProps)) { | ||
if (key === 'children') { | ||
if (typeof value === 'string' || typeof value === 'number') { | ||
element.childNodes[0].textContent = value; | ||
node.childNodes[0].textContent = value; | ||
} | ||
} else if (key === 'style') { | ||
Object.assign(element.style, value); | ||
Object.assign(node.style, value); | ||
} else if (key === 'unstable__transformChildren') { | ||
element.unstable__transformChildren = value; // eslint-disable-line camelcase | ||
node.unstable__transformChildren = value; // eslint-disable-line camelcase | ||
} else if (key === 'static') { | ||
element.static = true; | ||
node.static = true; | ||
} else { | ||
element.setAttribute(key, value); | ||
(0, _dom.setAttribute)(node, key, value); | ||
} | ||
@@ -99,8 +101,13 @@ } | ||
}, | ||
commitTextUpdate: (element, oldText, newText) => { | ||
element.textContent = newText; | ||
commitTextUpdate: (node, oldText, newText) => { | ||
if (node.nodeName === '#text') { | ||
node.nodeValue = newText; | ||
} else { | ||
node.textContent = newText; | ||
} | ||
onRender(); | ||
}, | ||
removeChild: (parent, child) => { | ||
parent.removeChild(child); | ||
removeChild: (parentNode, childNode) => { | ||
(0, _dom.removeChildNode)(parentNode, childNode); | ||
onRender(); | ||
@@ -107,0 +114,0 @@ } |
@@ -12,4 +12,2 @@ "use strict"; | ||
var _undom = _interopRequireDefault(require("undom")); | ||
var _applyStyles = _interopRequireDefault(require("./apply-styles")); | ||
@@ -19,21 +17,33 @@ | ||
var _cloneElement = _interopRequireDefault(require("./clone-element")); | ||
var _dom = require("./dom"); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const buildElement = (element, { | ||
config, | ||
terminalWidth, | ||
skipStaticElements | ||
}) => { | ||
const node = _yogaLayoutPrebuilt.default.Node.create(config); | ||
const measureText = text => { | ||
const width = (0, _widestLine.default)(text); | ||
const height = text.split('\n').length; | ||
return { | ||
width, | ||
height | ||
}; | ||
}; // Traverse the node tree, create Yoga nodes and assign styles to each Yoga node | ||
const style = element.style || {}; | ||
element.yogaNode = node; // Root element of the tree | ||
if (element.nodeName === 'BODY') { | ||
node.setWidth(terminalWidth); | ||
const buildLayout = (node, options) => { | ||
const { | ||
config, | ||
terminalWidth, | ||
skipStaticElements | ||
} = options; | ||
if (element.childNodes.length > 0) { | ||
const childNodes = element.childNodes.filter(childNode => { | ||
const yogaNode = _yogaLayoutPrebuilt.default.Node.create(config); | ||
node.yogaNode = yogaNode; | ||
const style = node.style || {}; // Root node of the tree | ||
if (node.nodeName === 'ROOT') { | ||
yogaNode.setWidth(terminalWidth); | ||
if (node.childNodes.length > 0) { | ||
const childNodes = node.childNodes.filter(childNode => { | ||
return skipStaticElements ? !childNode.static : true; | ||
@@ -43,49 +53,45 @@ }); | ||
for (const [index, childNode] of Object.entries(childNodes)) { | ||
const { | ||
yogaNode | ||
} = buildElement(childNode, { | ||
config, | ||
terminalWidth, | ||
skipStaticElements | ||
}); | ||
node.insertChild(yogaNode, index); | ||
const childYogaNode = buildLayout(childNode, options).yogaNode; | ||
yogaNode.insertChild(childYogaNode, index); | ||
} | ||
} | ||
return element; | ||
} // Element which has text node as the only child | ||
return node; | ||
} // Apply margin, padding, flex, etc styles | ||
if (element.textContent) { | ||
const width = (0, _widestLine.default)(element.textContent); | ||
const height = element.textContent.split('\n').length; | ||
node.setWidth(style.width || width); | ||
node.setHeight(style.height || height); | ||
(0, _applyStyles.default)(node, style); | ||
return element; | ||
(0, _applyStyles.default)(yogaNode, style); // Nodes with only text have a child Yoga node dedicated for that text | ||
if (node.textContent) { | ||
const { | ||
width, | ||
height | ||
} = measureText(node.textContent); | ||
yogaNode.setWidth(style.width || width); | ||
yogaNode.setHeight(style.height || height); | ||
return node; | ||
} // Text node | ||
if (element.nodeValue) { | ||
const width = (0, _widestLine.default)(element.nodeValue); | ||
const height = element.nodeValue.split('\n').length; | ||
node.setWidth(width); | ||
node.setHeight(height); | ||
(0, _applyStyles.default)(node, style); | ||
return element; | ||
} // All the other elements | ||
if (node.nodeValue) { | ||
const { | ||
width, | ||
height | ||
} = measureText(node.nodeValue); | ||
yogaNode.setWidth(width); | ||
yogaNode.setHeight(height); | ||
return node; | ||
} // Nodes with other nodes as children | ||
if (style.width) { | ||
node.setWidth(style.width); | ||
yogaNode.setWidth(style.width); | ||
} | ||
if (style.height) { | ||
node.setHeight(style.height); | ||
yogaNode.setHeight(style.height); | ||
} | ||
(0, _applyStyles.default)(node, style); | ||
if (element.childNodes.length > 0) { | ||
const childNodes = element.childNodes.filter(childNode => { | ||
if (node.childNodes.length > 0) { | ||
const childNodes = node.childNodes.filter(childNode => { | ||
return skipStaticElements ? !childNode.static : true; | ||
@@ -96,115 +102,115 @@ }); | ||
const { | ||
yogaNode | ||
} = buildElement(childNode, { | ||
config, | ||
terminalWidth, | ||
skipStaticElements | ||
}); | ||
node.insertChild(yogaNode, index); | ||
yogaNode: childYogaNode | ||
} = buildLayout(childNode, options); | ||
yogaNode.insertChild(childYogaNode, index); | ||
} | ||
} | ||
return element; | ||
}; | ||
return node; | ||
}; // After nodes are laid out, render each to output object, which later gets rendered to terminal | ||
const renderElement = (element, output, offsetX = 0, offsetY = 0, { | ||
const renderNodeToOutput = (node, output, offsetX = 0, offsetY = 0, { | ||
transformers, | ||
skipStaticElements | ||
}) => { | ||
if (element.static && skipStaticElements) { | ||
if (node.static && skipStaticElements) { | ||
return; | ||
} | ||
const node = element.yogaNode; | ||
const x = offsetX + node.getComputedLeft(); | ||
const y = offsetY + node.getComputedTop(); | ||
const { | ||
yogaNode | ||
} = node; // Left and top positions in Yoga are relative to their parent node | ||
const x = offsetX + yogaNode.getComputedLeft(); | ||
const y = offsetY + yogaNode.getComputedTop(); // Transformers are functions that transform final text output of each component | ||
// See Output class for logic that applies transformers | ||
let newTransformers = transformers; | ||
if (element.unstable__transformChildren) { | ||
newTransformers = [element.unstable__transformChildren, ...transformers]; | ||
} // Element with a text node | ||
if (node.unstable__transformChildren) { | ||
newTransformers = [node.unstable__transformChildren, ...transformers]; | ||
} | ||
const newOptions = { | ||
transformers: newTransformers, | ||
skipStaticElements | ||
}; // Text nodes | ||
if (element.textContent && element.nodeName !== '#text') { | ||
output.write(x, y, element.textContent, { | ||
transformers: newTransformers, | ||
skipStaticElements | ||
}); | ||
return; | ||
} // Text node | ||
const text = node.textContent || node.nodeValue; | ||
if (element.nodeName === '#text') { | ||
output.write(x, y, element.nodeValue, { | ||
transformers: newTransformers, | ||
skipStaticElements | ||
}); | ||
if (text) { | ||
output.write(x, y, text, newOptions); | ||
return; | ||
} // All the other elements who have children | ||
} // Nodes that have other nodes as children | ||
for (const childElement of element.childNodes) { | ||
renderElement(childElement, output, x, y, { | ||
transformers: newTransformers, | ||
skipStaticElements | ||
}); | ||
for (const childNode of node.childNodes) { | ||
renderNodeToOutput(childNode, output, x, y, newOptions); | ||
} | ||
}; | ||
}; // Since <Static> components can be placed anywhere in the tree, this helper finds and returns them | ||
const getStaticElements = element => { | ||
const staticElements = []; | ||
const getStaticNodes = element => { | ||
const staticNodes = []; | ||
for (const childNode of element.childNodes) { | ||
if (childNode.static) { | ||
staticElements.push(childNode); | ||
staticNodes.push(childNode); | ||
} | ||
if (childNode.childNodes.length > 0) { | ||
staticElements.push(...getStaticElements(childNode)); | ||
if (Array.isArray(childNode.childNodes) && childNode.childNodes.length > 0) { | ||
staticNodes.push(...getStaticNodes(childNode)); | ||
} | ||
} | ||
return staticElements; | ||
}; | ||
return staticNodes; | ||
}; // Build layout, apply styles, build text output of all nodes and return it | ||
var _default = ({ | ||
terminalWidth | ||
}) => { | ||
const config = _yogaLayoutPrebuilt.default.Config.create(); // Used to free up memory used by last node tree | ||
const config = _yogaLayoutPrebuilt.default.Config.create(); // Used to free up memory used by last Yoga node tree | ||
let lastNode; | ||
let lastStaticNode; | ||
return element => { | ||
if (lastNode) { | ||
lastNode.freeRecursive(); | ||
let lastYogaNode; | ||
let lastStaticYogaNode; | ||
return node => { | ||
if (lastYogaNode) { | ||
lastYogaNode.freeRecursive(); | ||
} | ||
if (lastStaticNode) { | ||
lastStaticNode.freeRecursive(); | ||
if (lastStaticYogaNode) { | ||
lastStaticYogaNode.freeRecursive(); | ||
} | ||
const staticElements = getStaticElements(element); | ||
const staticElements = getStaticNodes(node); | ||
if (staticElements.length > 1) { | ||
console.error('Warning: There can only be one <Static> component'); | ||
} | ||
if (process.env.NODE_ENV !== 'production') { | ||
console.error('Warning: There can only be one <Static> component'); | ||
} | ||
} // <Static> component must be built and rendered separately, so that the layout of the other output is unaffected | ||
let staticOutput; | ||
if (staticElements.length === 1) { | ||
const document = (0, _undom.default)(); | ||
document.body.appendChild((0, _cloneElement.default)(document, staticElements[0])); | ||
const staticNode = buildElement(document.body, { | ||
const rootNode = (0, _dom.createNode)('root'); | ||
(0, _dom.appendChildNode)(rootNode, staticElements[0]); | ||
const { | ||
yogaNode: staticYogaNode | ||
} = buildLayout(rootNode, { | ||
config, | ||
terminalWidth, | ||
skipStaticElements: false | ||
}).yogaNode; | ||
staticNode.calculateLayout(_yogaLayoutPrebuilt.default.UNDEFINED, _yogaLayoutPrebuilt.default.UNDEFINED, _yogaLayoutPrebuilt.default.DIRECTION_LTR); // Save current node tree to free up memory later | ||
}); | ||
staticYogaNode.calculateLayout(_yogaLayoutPrebuilt.default.UNDEFINED, _yogaLayoutPrebuilt.default.UNDEFINED, _yogaLayoutPrebuilt.default.DIRECTION_LTR); // Save current Yoga node tree to free up memory later | ||
lastStaticNode = staticNode; | ||
lastStaticYogaNode = staticYogaNode; | ||
staticOutput = new _output.default({ | ||
height: staticNode.getComputedHeight() | ||
height: staticYogaNode.getComputedHeight() | ||
}); | ||
renderElement(document.body, staticOutput, 0, 0, { | ||
renderNodeToOutput(rootNode, staticOutput, 0, 0, { | ||
transformers: [], | ||
@@ -215,14 +221,16 @@ skipStaticElements: false | ||
const node = buildElement(element, { | ||
const { | ||
yogaNode | ||
} = buildLayout(node, { | ||
config, | ||
terminalWidth, | ||
skipStaticElements: true | ||
}).yogaNode; | ||
node.calculateLayout(_yogaLayoutPrebuilt.default.UNDEFINED, _yogaLayoutPrebuilt.default.UNDEFINED, _yogaLayoutPrebuilt.default.DIRECTION_LTR); // Save current node tree to free up memory later | ||
}); | ||
yogaNode.calculateLayout(_yogaLayoutPrebuilt.default.UNDEFINED, _yogaLayoutPrebuilt.default.UNDEFINED, _yogaLayoutPrebuilt.default.DIRECTION_LTR); // Save current node tree to free up memory later | ||
lastNode = node; | ||
lastYogaNode = yogaNode; | ||
const output = new _output.default({ | ||
height: node.getComputedHeight() | ||
height: yogaNode.getComputedHeight() | ||
}); | ||
renderElement(element, output, 0, 0, { | ||
renderNodeToOutput(node, output, 0, 0, { | ||
transformers: [], | ||
@@ -229,0 +237,0 @@ skipStaticElements: true |
@@ -8,2 +8,10 @@ "use strict"; | ||
/** | ||
* Naive string diff algorithm | ||
* | ||
* It's used to detect only new additions to the previous string and it expects previous string to never change. | ||
* This function subtracts previous output from the new output and returns the difference. | ||
* | ||
* Used only for diffing output of <Static> component. | ||
*/ | ||
var _default = (previous, next) => { | ||
@@ -20,14 +28,15 @@ if (!previous) { | ||
const nextLines = next.split('\n'); | ||
const lineCount = Math.max(previousLines.length, nextLines.length); | ||
const diff = []; | ||
for (let lineIndex = 0; lineIndex < lineCount; lineIndex++) { | ||
if (previousLines[lineIndex] !== nextLines[lineIndex]) { | ||
diff.push(nextLines[lineIndex]); | ||
} | ||
if (previousLines.length === nextLines.length) { | ||
return next; | ||
} | ||
return diff.join('\n'); | ||
if (nextLines.length < previousLines.length) { | ||
throw new TypeError('Output of <Static> component has become smaller. <Static> component requires existing children to stay the same in order to correctly detect new children and write them to output stream. Ensure that only new children get added to <Static> component and existing children produce the same output on every render.'); | ||
} | ||
const diffStartIndex = nextLines.length - previousLines.length; | ||
return nextLines.slice(diffStartIndex + 1).join('\n'); | ||
}; | ||
exports.default = _default; |
@@ -14,2 +14,10 @@ "use strict"; | ||
/** | ||
* "Virtual" output class | ||
* | ||
* Handles the positioning and saving of the output of each node in the tree. | ||
* Also responsible for applying transformations to each character of the output. | ||
* | ||
* Used to generate the final output of all nodes before writing it to actual output stream (e.g. stdout) | ||
*/ | ||
class Output { | ||
@@ -19,2 +27,3 @@ constructor({ | ||
}) { | ||
// Initialize output array with a specific set of rows, so that margin/padding at the bottom is preserved | ||
this.output = new Array(height); | ||
@@ -35,4 +44,9 @@ } | ||
this.output[y + offsetY] = []; | ||
} | ||
} // Since number of characters displayed visually isn't equal to actual number of characters | ||
// because of ANSI escapes, use `sliceAnsi` module to retrieve actual character along with | ||
// ANSI escapes that wrap it and apply transformations to it | ||
// | ||
// It results in a lot more ANSI escapes in the output, but it produces correct output | ||
let char = (0, _sliceAnsi.default)(line, offsetX, offsetX + 1); | ||
@@ -53,6 +67,10 @@ | ||
let ret = ''; | ||
const rows = this.output.length; | ||
for (let y = 0; y < this.output.length; y++) { | ||
for (let y = 0; y < rows; y++) { | ||
if (this.output[y]) { | ||
for (let x = 0; x < this.output[y].length; x++) { | ||
const columns = this.output[y].length; | ||
for (let x = 0; x < columns; x++) { | ||
// Treat empty columns as spaces | ||
ret += this.output[y][x] || ' '; | ||
@@ -59,0 +77,0 @@ } |
@@ -10,4 +10,2 @@ "use strict"; | ||
var _undom = _interopRequireDefault(require("undom")); | ||
var _logUpdate = _interopRequireDefault(require("log-update")); | ||
@@ -23,2 +21,4 @@ | ||
var _dom = require("./dom"); | ||
var _App = _interopRequireDefault(require("./components/App")); | ||
@@ -33,2 +33,3 @@ | ||
var _default = (node, options = {}) => { | ||
// Stream was passed instead of `options` object | ||
if (typeof options.write === 'function') { | ||
@@ -46,3 +47,3 @@ options = { | ||
}, options); | ||
const document = (0, _undom.default)(); | ||
const rootNode = (0, _dom.createNode)('root'); | ||
const render = (0, _createRenderer.default)({ | ||
@@ -55,4 +56,5 @@ terminalWidth: options.stdout.columns | ||
let ignoreRender = false; // Store last <Static> output to only rerender when needed | ||
let ignoreRender = false; // Store last output to only rerender when needed | ||
let lastOutput = ''; | ||
let lastStaticOutput = ''; | ||
@@ -68,3 +70,3 @@ | ||
staticOutput | ||
} = render(document.body); | ||
} = render(rootNode); | ||
@@ -82,13 +84,17 @@ if (options.debug) { | ||
options.stdout.write((0, _diffString.default)(lastStaticOutput, staticOutput)); | ||
log(output); | ||
lastStaticOutput = staticOutput; | ||
} | ||
log(output); | ||
if (output !== lastOutput) { | ||
log(output); | ||
lastOutput = output; | ||
} | ||
}; | ||
const debouncedRender = options.debug ? onRender : (0, _lodash.default)(onRender, 50, { | ||
const throttledRender = options.debug ? onRender : (0, _lodash.default)(onRender, 50, { | ||
leading: true, | ||
trailing: true | ||
}); | ||
const reconciler = options.stdout._inkReconciler || (0, _createReconciler.default)(document, debouncedRender); | ||
const reconciler = options.stdout._inkReconciler || (0, _createReconciler.default)(throttledRender); | ||
@@ -98,3 +104,3 @@ if (!options.stdout._ink) { | ||
options.stdout._inkReconciler = reconciler; | ||
options.stdout._inkContainer = reconciler.createContainer(document.body, false); | ||
options.stdout._inkContainer = reconciler.createContainer(rootNode, false); | ||
} | ||
@@ -109,4 +115,4 @@ | ||
return () => { | ||
if (typeof debouncedRender.cancel === 'function') { | ||
debouncedRender.cancel(); | ||
if (typeof throttledRender.cancel === 'function') { | ||
throttledRender.cancel(); | ||
onRender(); | ||
@@ -113,0 +119,0 @@ log.done(); |
{ | ||
"name": "ink", | ||
"version": "2.0.0-6", | ||
"version": "2.0.0-7", | ||
"description": "React for CLI", | ||
@@ -50,3 +50,2 @@ "license": "MIT", | ||
"string-length": "^2.0.0", | ||
"undom": "^0.4.0", | ||
"widest-line": "^2.0.0", | ||
@@ -53,0 +52,0 @@ "yoga-layout-prebuilt": "^1.9.3" |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
57865
11
22
1033
2
- Removedundom@^0.4.0
- Removedundom@0.4.0(transitive)