From 9ab7d2aab256a69b338794bd57d58cc2df5f5fb7 Mon Sep 17 00:00:00 2001 From: Mike Grip Date: Wed, 18 Jul 2018 12:32:28 -0400 Subject: [PATCH] Refactor sections * Convert output algorithm to structured classes * Add border styles to sections --- .../__snapshots__/section-test.js.snap | 17 + .../__snapshots__/kitchen-sink-test.js.snap | 26 +- .../integration-tests/kitchen-sink-test.js | 42 ++ src/__tests__/section-test.js | 53 +++ src/components.js | 106 ++++++ src/index.js | 201 +--------- src/output.js | 360 ++++++++++++++++++ src/reconciler.js | 8 +- 8 files changed, 621 insertions(+), 192 deletions(-) create mode 100644 src/components.js create mode 100644 src/output.js diff --git a/src/__tests__/__snapshots__/section-test.js.snap b/src/__tests__/__snapshots__/section-test.js.snap index 13284c3..b23fce7 100644 --- a/src/__tests__/__snapshots__/section-test.js.snap +++ b/src/__tests__/__snapshots__/section-test.js.snap @@ -15,6 +15,23 @@ exports[`sections should be able to align text right 1`] = ` " `; +exports[`sections should be able to render a border 1`] = ` +"-------------------------------------------------- +|++++++++++++Test section with border++++++++++++| +-------------------------------------------------- +" +`; + +exports[`sections should be able to render a border 2`] = ` +"************************************************** +*Some Text+++++++++++++++-------------------------* +*++++++++++++++++++++++++|+++Test section with+++|* +*++++++++++++++++++++++++|++++++++border+++++++++|* +*++++++++++++++++++++++++-------------------------* +************************************************** +" +`; + exports[`sections should be able to render horizontally 1`] = ` "Column 1+++++++++++++++++Column 2+++++++++++++++++ " diff --git a/src/__tests__/integration-tests/__snapshots__/kitchen-sink-test.js.snap b/src/__tests__/integration-tests/__snapshots__/kitchen-sink-test.js.snap index d13c14e..8ca6697 100644 --- a/src/__tests__/integration-tests/__snapshots__/kitchen-sink-test.js.snap +++ b/src/__tests__/integration-tests/__snapshots__/kitchen-sink-test.js.snap @@ -2,7 +2,7 @@ exports[`Columns and rows should work together 1`] = ` "Some text+++++++++++++++++++++++++++++++++++++++++ -Column A++++++++Column B++++++++Column C++++++++ +Column A++++++++Column B++++++++Column C++++++++++ Other text++++++++++++++++++++++++++++++++++++++++ " `; @@ -18,3 +18,27 @@ exports[`Columns should work nested within other columns 1`] = ` ++++++++++++++++++wrap++++++++++++++++++++++++++ " `; + +exports[`Columns, rows, section styles should all work together 1`] = ` +"################################################## +#++++++++++++++++++++Some App++++++++++++++++++++# +#*************************-------------------------# +#*+++++++✔︎ Step 1+++++++*|Some messages for this+|# +#*+++++++◯ Step 2++++++++*|app++++++++++++++++++++|# +#*+++++++◯ Step 3++++++++*-------------------------# +#*************************+++++++++++++++++++++++++# +#+++++++Some stuff for this app is done! 🤘++++++++# +#+Heres some more informative stuff about your app+# +#+++++++++++++++++++++browser++++++++++++++++++++++# +#++++++++++++++++++++++↙↗ ↖↘+++++++++++++++++++++++# +#--------------------------------------------------# +#|++++++++server+++++++++||++++++dev-server+++++++|# +#|++(initial response)+++||+++++(app bundle)++++++|# +#|++++localhost:3000+++++||++++localhost:8080+++++|# +#-------------------------|+websocket server (for+|# +#+++++++++++++++++++++++++|+++++++++HMR)++++++++++|# +#+++++++++++++++++++++++++|++++localhost:8081+++++|# +#+++++++++++++++++++++++++-------------------------# +################################################## +" +`; diff --git a/src/__tests__/integration-tests/kitchen-sink-test.js b/src/__tests__/integration-tests/kitchen-sink-test.js index 6ae1bb8..aed25fc 100644 --- a/src/__tests__/integration-tests/kitchen-sink-test.js +++ b/src/__tests__/integration-tests/kitchen-sink-test.js @@ -31,3 +31,45 @@ TestRender(
Column C
); + +TestRender( + "Columns, rows, section styles should all work together", +
+ Some App +
+
+
+ ✔︎ Step 1 +
+ ◯ Step 2 +
+ ◯ Step 3 +
+
+ Some messages for this app +
+
+
+ Some stuff for this app is done! 🤘 +
+ Heres some more informative stuff about your app +
+ browser +
+ ↙↗ ↖↘ +
+
+ server +
+ (initial response) +
+ localhost:3000 +
+
+ dev-server
(app bundle)
localhost:8080
websocket server + (for HMR)
localhost:8081 +
+
+
+
+); diff --git a/src/__tests__/section-test.js b/src/__tests__/section-test.js index 421bce8..e36a53c 100644 --- a/src/__tests__/section-test.js +++ b/src/__tests__/section-test.js @@ -71,3 +71,56 @@ test("sections should be able to align text center", done => { "+" ); }); + +test("sections should be able to render a border", done => { + ReactCLI.render( +
+
+ Test section with border +
+
, + undefined, + 50, + outputString => { + expect(outputString).toMatchSnapshot(); + done(); + }, + "+" + ); + + ReactCLI.render( +
+ Some Text +
+ Test section with border +
+
, + undefined, + 50, + outputString => { + expect(outputString).toMatchSnapshot(); + done(); + }, + "+" + ); +}); diff --git a/src/components.js b/src/components.js new file mode 100644 index 0000000..5353ac8 --- /dev/null +++ b/src/components.js @@ -0,0 +1,106 @@ +// @flow strict + +import wrapAnsiNewLine from "wrap-ansi"; +// this module is helpful for dealing with ansi characters, but it returns a +// string with embedded new lines. We need it as an array, so we'll split it here +const wrapAnsi = (input: string, columns: number): Array => + wrapAnsiNewLine(input, columns).split("\n"); + +class Border { + vertical: ?string; + horizontal: ?string; + cornerTopLeft: ?string; + cornerTopRight: ?string; + cornerBottomLeft: ?string; + cornerBottomRight: ?string; + + constructor({ + vertical, + horizontal, + cornerTopLeft, + cornerTopRight, + cornerBottomLeft, + cornerBottomRight + }) { + this.vertical = vertical; + this.horizontal = horizontal; + this.cornerTopLeft = cornerTopLeft; + this.cornerTopRight = cornerTopRight; + this.cornerBottomLeft = cornerBottomLeft; + this.cornerBottomRight = cornerBottomRight; + } + + horizontalWidth(): number { + return ( + Math.max( + this.vertical ? this.vertical.length : 0, + this.cornerTopLeft ? this.cornerTopLeft.length : 0, + this.cornerBottomLeft ? this.cornerBottomLeft.length : 0 + ) + + Math.max( + this.vertical ? this.vertical.length : 0, + this.cornerTopRight ? this.cornerTopRight.length : 0, + this.cornerBottomRight ? this.cornerBottomRight.length : 0 + ) + ); + } + + verticalHeight(): number { + return ( + Math.max( + this.horizontal ? this.horizontal.length : 0, + this.cornerTopLeft ? this.cornerTopLeft.length : 0, + this.cornerTopRight ? this.cornerTopRight.length : 0 + ) + + Math.max( + this.horizontal ? this.horizontal.length : 0, + this.cornerBottomLeft ? this.cornerBottomLeft.length : 0, + this.cornerBottomRight ? this.cornerBottomRight.length : 0 + ) + ); + } +} +export class Section { + orientation: "vertical" | "horizontal"; + align: "left" | "center" | "right"; + children: Array
= []; + border: Border; + static type: "div" = "div"; + + constructor({ + useHorizontalOrientation = false, + align = "left", + border = {} + }: { + useHorizontalOrientation: boolean, + align: "left" | "center" | "right", + border: { + vertical?: string, + horizontal?: string, + cornerTopLeft?: string, + cornerTopRight?: string, + cornerBottomLeft?: string, + cornerBottomRight?: string + } + }) { + this.orientation = useHorizontalOrientation ? "horizontal" : "vertical"; + this.align = align; + this.border = new Border(border); + } + + convertTextToArray(text: Text, totalWidth: number): Array { + return wrapAnsi(text.text, totalWidth - this.border.horizontalWidth()); + } +} + +export class Text { + text: string; + + constructor(text: string) { + this.text = text; + } +} + +export class Break { + static type: "br" = "br"; +} diff --git a/src/index.js b/src/index.js index 9d16549..328bfbb 100644 --- a/src/index.js +++ b/src/index.js @@ -2,39 +2,9 @@ import Reconciler from "./reconciler"; import * as React from "react"; -import wrapAnsiNewLine from "wrap-ansi"; import logUpdate from "log-update"; -// this module is helpful for dealing with ansi characters, but it returns a -// string with embedded new lines. We need it as an array, so we'll split it here -const wrapAnsi = (input: string, columns: number): Array => - wrapAnsiNewLine(input, columns).split("\n"); - -export class Section { - orientation: "vertical" | "horizontal"; - align: "left" | "center" | "right"; - children: Array
= []; - static type: "div" = "div"; - - constructor( - useHorizontalOrientation: boolean = false, - align: "left" | "center" | "right" = "left" - ) { - this.orientation = useHorizontalOrientation ? "horizontal" : "vertical"; - this.align = align; - } -} - -export class Text { - text: string; - - constructor(text: string) { - this.text = text; - } -} - -export class Break { - static type: "br" = "br"; -} +import { Section } from "./components"; +import getOutputFromSection from "./output"; class Console { consoleWidth: number; @@ -62,166 +32,19 @@ class Console { } update() { - type ColumnOutput = { - width: number, - columns: Array, - type: "horizontal" - }; - type RowOutput = { - width: number, - rows: Array, - type: "vertical" - }; - type TextOutput = { - width: number, - text: Array, - type: "text", - align: "left" | "center" | "right" - }; - type OutputType = ColumnOutput | RowOutput | TextOutput; - const widthCheck = (section: Section, totalWidth: number): OutputType => { - // If we have multiple text nodes in a row, first combine them into one - const combinedChildren: Array = []; - let textBuffer = []; - section.children.forEach((child, index) => { - if (child instanceof Text) { - textBuffer.push(child); - } else { - if (textBuffer.length > 0) { - combinedChildren.push( - new Text(textBuffer.map(textObject => textObject.text).join("")) - ); - textBuffer = []; - } - combinedChildren.push(child); - } - if (index === section.children.length - 1 && textBuffer.length > 0) { - combinedChildren.push( - new Text(textBuffer.map(textObject => textObject.text).join("")) - ); - } - }); - if (section.orientation === "horizontal") { - const columnNumber = section.children.reduce( - (acc, child) => (child instanceof Break ? acc : acc + 1), - 0 - ); - return { - width: totalWidth, - columns: combinedChildren.reduce((acc, child) => { - if (child instanceof Section) { - return acc.concat(widthCheck(child, totalWidth / columnNumber)); - } else if (child instanceof Text) { - return acc.concat({ - width: totalWidth / columnNumber, - text: wrapAnsi(child.text, totalWidth / columnNumber), - type: "text", - align: section.align - }); - } - return acc; - }, []), - type: "horizontal" - }; - } else { - return { - width: totalWidth, - rows: combinedChildren.reduce((acc, child) => { - if (child instanceof Section) { - return acc.concat(widthCheck(child, totalWidth)); - } else if (child instanceof Text) { - return acc.concat({ - width: totalWidth, - text: wrapAnsi(child.text, totalWidth), - type: "text", - align: section.align - }); - } - return acc; - }, []), - type: "vertical" - }; - } - }; - const output = widthCheck(this.root, this.consoleWidth); - - const getOutputLineLength = (outputObject: OutputType): number => { - if (outputObject.type === "horizontal") { - return outputObject.columns.reduce((max, column) => { - const columnLength = getOutputLineLength(column); - return columnLength > max ? columnLength : max; - }, 0); - } else if (outputObject.type === "text") { - return outputObject.text.length; - } else { - return outputObject.rows.reduce((acc, child) => { - return acc + getOutputLineLength(child); - }, 0); - } - }; + const output = getOutputFromSection({ + section: this.root, + width: this.consoleWidth + }); let outputString = ""; let currentLineIndex = 0; - const generateOutput = ( - outputObject: OutputType, - lineIndex: number - ): void => { - if (outputObject.type === "horizontal") { - const maxColumnHeight = outputObject.columns.reduce((acc, column) => { - const currentColumnHeight = getOutputLineLength(column); - return currentColumnHeight > acc ? currentColumnHeight : acc; - }, 0); - outputObject.columns.forEach(column => { - if (currentLineIndex >= lineIndex + getOutputLineLength(column)) { - if (currentLineIndex < lineIndex + maxColumnHeight) { - outputString += "".padEnd(column.width, this.spacing); - } - } else { - generateOutput(column, lineIndex); - } - }); - } else if (outputObject.type === "text") { - if ( - currentLineIndex >= lineIndex && - currentLineIndex < lineIndex + outputObject.text.length - ) { - const potentialOutput = - outputObject.text[currentLineIndex - lineIndex]; - switch (outputObject.align) { - case "left": - outputString += (potentialOutput || "").padEnd( - outputObject.width, - this.spacing - ); - break; - case "right": - outputString += (potentialOutput || "").padStart( - outputObject.width, - this.spacing - ); - break; - case "center": - outputString += (potentialOutput || "") - .padStart( - outputObject.width / 2 + potentialOutput.length / 2, - this.spacing - ) - .padEnd(outputObject.width, this.spacing); - break; - } - } - } else { - let tempLineIndex = lineIndex; - outputObject.rows.forEach(child => { - if (tempLineIndex <= currentLineIndex) { - generateOutput(child, tempLineIndex); - tempLineIndex += getOutputLineLength(child); - } - }); - } - }; - while (currentLineIndex < getOutputLineLength(output)) { - generateOutput(output, 0); + while (currentLineIndex < output.getLineLength()) { + outputString += output.generateOutput({ + currentLineIndex, + spacing: this.spacing, + startLineIndex: 0 + }); outputString += "\n"; currentLineIndex++; } diff --git a/src/output.js b/src/output.js new file mode 100644 index 0000000..d473a9e --- /dev/null +++ b/src/output.js @@ -0,0 +1,360 @@ +// @flow strict + +import { Section, Text, Break } from "./components"; + +function combineChildren(section: Section): Array { + // If we have multiple text nodes in a row, first combine them into one + const combinedChildren: Array = []; + let textBuffer = []; + section.children.forEach((child, index) => { + if (child instanceof Text) { + textBuffer.push(child); + } else { + if (textBuffer.length > 0) { + combinedChildren.push( + new Text(textBuffer.map(textObject => textObject.text).join("")) + ); + textBuffer = []; + } + combinedChildren.push(child); + } + if (index === section.children.length - 1 && textBuffer.length > 0) { + combinedChildren.push( + new Text(textBuffer.map(textObject => textObject.text).join("")) + ); + } + }); + return combinedChildren; +} + +export default function getOutputFromSection({ + section, + width +}: { + section: Section, + width: number +}): RowOutput | ColumnOutput { + if (section.orientation === "vertical") { + return new RowOutput({ width, section }); + } else { + return new ColumnOutput({ width, section }); + } +} + +class RowOutput { + width: number; + rows: Array; + section: Section; + + constructor({ width, section }: { width: number, section: Section }) { + this.width = width; + this.section = section; + + const combinedChildren = combineChildren(section); + this.rows = combinedChildren.reduce((acc, child) => { + if (child instanceof Section) { + return acc.concat( + getOutputFromSection({ section: child, width: width }) + ); + } else if (child instanceof Text) { + return acc.concat( + new TextOutput(section.convertTextToArray(child, width)) + ); + } + return acc; + }, []); + } + + getLineLength(): number { + const rowsLineLength = this.rows.reduce((acc, child) => { + return acc + child.getLineLength(); + }, 0); + return rowsLineLength + this.section.border.verticalHeight(); + } + + padText({ text, spacing }: { text: string, spacing: string }): string { + const border = this.section.border.vertical || ""; + + let innerText; + switch (this.section.align) { + case "left": + innerText = text.padEnd(this.width - border.length * 2, spacing); + break; + case "right": + innerText = text.padStart(this.width - border.length * 2, spacing); + break; + default: + innerText = text + .padStart( + (this.width - border.length * 2) / 2 + text.length / 2, + spacing + ) + .padEnd(this.width - border.length * 2, spacing); + break; + } + return border + innerText + border; + } + + generateOutput({ + currentLineIndex, + startLineIndex, + spacing + }: { + currentLineIndex: number, + startLineIndex: number, + spacing: string + }): string { + const topBorderLength = Math.max( + (this.section.border.horizontal || "").length, + (this.section.border.cornerTopLeft || "").length, + (this.section.border.cornerTopRight || "").length + ); + const bottomBorderLength = Math.max( + (this.section.border.horizontal || "").length, + (this.section.border.cornerBottomLeft || "").length, + (this.section.border.cornerBottomRight || "").length + ); + + let outputString = ""; + + // print top border + if ( + topBorderLength > 0 && + currentLineIndex < startLineIndex + topBorderLength && + currentLineIndex >= startLineIndex + ) { + // @TODO need to account for corners and borders more than 1 length + return outputString.padEnd( + this.width, + this.section.border.horizontal || "" + ); + } + + // print bottom border + if ( + bottomBorderLength > 0 && + currentLineIndex < startLineIndex + this.getLineLength() && + currentLineIndex >= + startLineIndex + this.getLineLength() - bottomBorderLength + ) { + // @TODO need to account for corners and borders more than 1 length + return outputString.padEnd( + this.width, + this.section.border.horizontal || "" + ); + } + + let tempLineIndex = startLineIndex + topBorderLength; + this.rows.forEach(child => { + if ( + currentLineIndex >= tempLineIndex && + currentLineIndex < tempLineIndex + child.getLineLength() + ) { + if (child instanceof RowOutput || child instanceof ColumnOutput) { + outputString += this.padText({ + spacing, + text: child.generateOutput({ + currentLineIndex, + startLineIndex: tempLineIndex, + spacing + }) + }); + } else if (child instanceof TextOutput) { + if ( + currentLineIndex >= tempLineIndex && + currentLineIndex < tempLineIndex + child.getLineLength() + ) { + const potentialOutput = + child.text[currentLineIndex - tempLineIndex] || ""; + outputString += this.padText({ spacing, text: potentialOutput }); + } + } + } + tempLineIndex += child.getLineLength(); + }); + return outputString; + } +} + +class ColumnOutput { + width: number; + columns: Array; + section: Section; + + constructor({ width, section }: { width: number, section: Section }) { + this.width = width; + this.section = section; + + const combinedChildren = combineChildren(section); + const columnNumber = combinedChildren.reduce( + (acc, child) => (child instanceof Break ? acc : acc + 1), + 0 + ); + this.columns = combinedChildren.reduce((acc, child) => { + if (child instanceof Section) { + return acc.concat( + getOutputFromSection({ section: child, width: width / columnNumber }) + ); + } else if (child instanceof Text) { + return acc.concat( + new TextOutput( + section.convertTextToArray(child, width / columnNumber) + ) + ); + } + return acc; + }, []); + } + + getLineLength(): number { + const maxColumnHeight = this.columns.reduce((max, column) => { + const columnLength = column.getLineLength(); + return columnLength > max ? columnLength : max; + }, 0); + return maxColumnHeight + this.section.border.verticalHeight(); + } + + padText({ + text, + spacing, + index + }: { + text: string, + spacing: string, + index: number + }): string { + const border = this.section.border.vertical || ""; + const innerWidth = this.width - this.section.border.horizontalWidth(); + let innerText; + + switch (this.section.align) { + case "left": + innerText = text.padEnd(innerWidth / this.columns.length, spacing); + break; + case "right": + innerText = text.padStart(innerWidth / this.columns.length, spacing); + break; + default: + innerText = text + .padStart( + innerWidth / this.columns.length / 2 + text.length / 2, + spacing + ) + .padEnd(innerWidth / this.columns.length, spacing); + break; + } + if (index === 0) { + return border + innerText; + } else if (index === this.columns.length - 1) { + return innerText + border; + } + return innerText; + } + + generateOutput({ + currentLineIndex, + startLineIndex, + spacing + }: { + currentLineIndex: number, + startLineIndex: number, + spacing: string + }): string { + const maxColumnHeight = this.columns.reduce((acc, column) => { + const currentColumnHeight = column.getLineLength(); + return currentColumnHeight > acc ? currentColumnHeight : acc; + }, 0); + const topBorderLength = Math.max( + (this.section.border.horizontal || "").length, + (this.section.border.cornerTopLeft || "").length, + (this.section.border.cornerTopRight || "").length + ); + const bottomBorderLength = Math.max( + (this.section.border.horizontal || "").length, + (this.section.border.cornerBottomLeft || "").length, + (this.section.border.cornerBottomRight || "").length + ); + + let outputString = ""; + + // print top border + if ( + topBorderLength > 0 && + currentLineIndex < startLineIndex + topBorderLength && + currentLineIndex >= startLineIndex + ) { + // @TODO need to account for corners and borders more than 1 length + return outputString.padEnd( + this.width, + this.section.border.horizontal || "" + ); + } + + // print bottom border + if ( + bottomBorderLength > 0 && + currentLineIndex < startLineIndex + this.getLineLength() && + currentLineIndex >= + startLineIndex + this.getLineLength() - bottomBorderLength + ) { + // @TODO need to account for corners and borders more than 1 length + return outputString.padEnd( + this.width, + this.section.border.horizontal || "" + ); + } + + this.columns.forEach((column, columnIndex) => { + if ( + currentLineIndex >= + startLineIndex + topBorderLength + column.getLineLength() + ) { + if ( + currentLineIndex < + startLineIndex + topBorderLength + maxColumnHeight + ) { + outputString += this.padText({ + text: "", + spacing, + index: columnIndex + }); + } + } else { + if (column instanceof RowOutput || column instanceof ColumnOutput) { + outputString += this.padText({ + text: column.generateOutput({ + currentLineIndex, + startLineIndex: startLineIndex + topBorderLength, + spacing + }), + spacing, + index: columnIndex + }); + } else if (column instanceof TextOutput) { + const potentialOutput = + column.text[currentLineIndex - topBorderLength - startLineIndex] || + ""; + outputString += this.padText({ + spacing, + text: potentialOutput, + index: columnIndex + }); + } + } + }); + return outputString; + } +} + +class TextOutput { + text: Array; + + constructor(text: Array) { + this.text = text; + } + + getLineLength(): number { + return this.text.length; + } +} +type OutputType = RowOutput | TextOutput | ColumnOutput; diff --git a/src/reconciler.js b/src/reconciler.js index 93efe03..9439764 100644 --- a/src/reconciler.js +++ b/src/reconciler.js @@ -1,6 +1,6 @@ // @flow strict -import { Section, Text, Break } from "./index"; +import { Section, Text, Break } from "./components"; import Reconciler from "react-reconciler"; const ReconcilerConfig = { @@ -14,7 +14,11 @@ const ReconcilerConfig = { case Break.type: return new Break(); case Section.type: - return new Section(props.horizontal, props.align); + return new Section({ + useHorizontalOrientation: props.horizontal, + align: props.align, + border: props.border + }); default: // throw error? return false;