├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── doc ├── all-glyph-combinations.txt ├── bitmap-registry.json ├── color.md ├── images │ ├── box-types.png │ └── color │ │ ├── chalk.png │ │ ├── lime.png │ │ └── modded.png ├── reference │ ├── half-line-relations.png │ ├── line-characters.png │ ├── line-glyphs.json │ ├── line-relations.png │ ├── named-border-sets.pdf │ ├── named-border-sets.png │ ├── placement-combinators.csv │ └── table-of-placement-combinators.png ├── relations-big.png ├── relations-small.png ├── relations.png ├── relations.svg └── screenshots │ ├── eg │ ├── 1-hello-world.ts.alacritty.png │ ├── 1-hello-world.ts.png │ ├── 2-mondrian.ts.png │ ├── 3-waves.ts.mkv │ ├── 3-waves.ts.png │ ├── 4-bar-chart.ts.png │ ├── 5-nested-borders.ts.png │ ├── 5-nested.borders.ts.alacritty.png │ ├── 6-flow.ts.png │ ├── 7-table.ts.alacritty.png │ ├── 7-table.ts.png │ ├── 8-logo-alacritty.ts.png │ ├── 8-logo.ts.png │ └── 9-blend-mode.ts.png │ ├── index.md │ └── reference │ ├── backdrops-kitty-fira-code.png │ ├── backdrops.ts.alacritty.png │ ├── backdrops.ts.png │ ├── blends.ts.png │ ├── border │ ├── masks.ts.png │ └── sets.ts.png │ ├── glyph │ ├── another.next-glyph.ts.png │ ├── bitmaps.ts.png │ ├── next-glyph.ts.png │ └── relations.ts.png │ └── placements.ts.png ├── package.json ├── repl ├── scripts ├── add-js-ext-to-dist.sh ├── build.sh ├── clean.sh ├── fix-dist-require.sh ├── record-examples.zsh ├── run-ts.sh └── test-repl-import.sh ├── src ├── align.ts ├── align │ ├── align.ts │ ├── instances.ts │ ├── lens.ts │ ├── tests │ │ └── instances.test.ts │ └── types.ts ├── backdrop.ts ├── backdrop │ ├── backdrop.ts │ ├── instances.ts │ ├── lens.ts │ ├── named.ts │ ├── paint.ts │ ├── tests │ │ ├── lens.test.ts │ │ ├── repeat.test.ts │ │ └── stretch.test.ts │ └── types.ts ├── bin │ ├── eg │ │ ├── 1-hello-world.ts │ │ ├── 2-mondrian.ts │ │ ├── 3-waves.ts │ │ ├── 4-bar-chart.ts │ │ ├── 5-nested-borders.ts │ │ ├── 6-flow.ts │ │ ├── 7-table.ts │ │ ├── 8-logo.ts │ │ └── 9-blend-mode.ts │ ├── helpers.ts │ └── reference │ │ ├── backdrops.ts │ │ ├── blends.ts │ │ ├── border │ │ ├── masks.ts │ │ └── sets.ts │ │ ├── glyph │ │ ├── bitmaps.ts │ │ ├── next-glyph.ts │ │ └── relations.ts │ │ └── placements.ts ├── bitmap.ts ├── bitmap │ ├── bitmap.ts │ ├── data.ts │ ├── data │ │ ├── cross.ts │ │ ├── elbow.ts │ │ ├── halftone.ts │ │ ├── line.ts │ │ ├── parse.ts │ │ ├── tee.ts │ │ ├── tests │ │ │ └── parse.test.ts │ │ └── types.ts │ ├── named.ts │ ├── named │ │ ├── classify.ts │ │ ├── elbow.ts │ │ ├── line.ts │ │ ├── other.ts │ │ └── types.ts │ ├── ops.ts │ ├── ops │ │ ├── instances.ts │ │ ├── matrix.ts │ │ ├── query.ts │ │ ├── row.ts │ │ ├── stack.ts │ │ ├── switches.ts │ │ └── tests │ │ │ ├── matrix.test.ts │ │ │ ├── query.test.ts │ │ │ ├── row.test.ts │ │ │ ├── stack.test.ts │ │ │ └── switches.test.ts │ ├── quadRes.ts │ ├── registry.ts │ ├── role.ts │ ├── rolesReport.ts │ └── tests │ │ ├── quadRes.test.ts │ │ └── registry.test.ts ├── block.ts ├── block │ ├── block.ts │ ├── build.ts │ ├── instances.ts │ ├── lens.ts │ ├── paint.ts │ ├── rect.ts │ ├── tests │ │ ├── basic.test.ts │ │ ├── instances.test.ts │ │ └── paint.test.ts │ └── types.ts ├── border.ts ├── border │ ├── apply.ts │ ├── backdrop.ts │ ├── border.ts │ ├── build.ts │ ├── edge.ts │ ├── lines.ts │ ├── mask.ts │ ├── ops.ts │ ├── part.ts │ ├── sets.ts │ ├── tests │ │ ├── apply.test.ts │ │ ├── edge.test.ts │ │ ├── helpers.ts │ │ ├── lines.test.ts │ │ ├── part.test.ts │ │ └── variants.test.ts │ ├── types.ts │ └── variants.ts ├── box.ts ├── box │ ├── align.ts │ ├── block.ts │ ├── box.ts │ ├── build.ts │ ├── cat.ts │ ├── margins.ts │ ├── move.ts │ ├── nodes.ts │ ├── paint.ts │ ├── place.ts │ ├── place │ │ ├── names.ts │ │ ├── ops.ts │ │ ├── placements.ts │ │ └── tests │ │ │ └── place.test.ts │ ├── rect.ts │ ├── report.ts │ ├── tests │ │ ├── basic.test.ts │ │ ├── cat.test.ts │ │ ├── helpers.ts │ │ ├── instances.test.ts │ │ ├── move.test.ts │ │ ├── paint.test.ts │ │ ├── pos.test.ts │ │ └── size.test.ts │ └── types.ts ├── boxes.ts ├── boxes │ ├── basic.ts │ ├── boxes.ts │ ├── flow.ts │ ├── labeled.ts │ ├── tests │ │ └── flow.test.ts │ └── win.ts ├── cell.ts ├── cell │ ├── build.ts │ ├── cell.ts │ ├── copy.ts │ ├── instances.ts │ ├── lens.ts │ ├── ops.ts │ ├── packed.ts │ ├── paint.ts │ ├── parse.ts │ ├── rune.ts │ ├── stack.ts │ ├── tests │ │ ├── build.test.ts │ │ ├── helpers.ts │ │ ├── instances.test.ts │ │ ├── ops.test.ts │ │ ├── packed.test.ts │ │ ├── rune.test.ts │ │ └── types.test.ts │ ├── type.ts │ └── types.ts ├── color.ts ├── color │ ├── blend.ts │ ├── color.ts │ ├── hsla.ts │ ├── instances.ts │ ├── lens.ts │ ├── opacity.ts │ ├── ops.ts │ ├── paint.ts │ ├── palette.ts │ ├── rgba.ts │ ├── tests │ │ ├── blend.test.ts │ │ ├── helpers.ts │ │ ├── instances.test.ts │ │ ├── lens.test.ts │ │ ├── ops.test.ts │ │ ├── paint.test.ts │ │ ├── rgba.test.ts │ │ └── types.test.ts │ └── types.ts ├── devBin │ ├── all-combinations.ts │ ├── precompute-bitmaps.ts │ ├── pure.ts │ ├── should-import.ts │ └── should-run.ts ├── geometry.ts ├── geometry │ ├── borderDir.ts │ ├── corner.ts │ ├── dir.ts │ ├── orientation.ts │ ├── pos.ts │ ├── rect.ts │ ├── size.ts │ ├── spacing.ts │ └── tests │ │ ├── borderDir.test.ts │ │ ├── corner.test.ts │ │ ├── dir.test.ts │ │ ├── helpers.ts │ │ ├── pos.test.ts │ │ ├── rect.test.ts │ │ └── size.test.ts ├── glyph.ts ├── glyph │ ├── combine │ │ ├── cachedStack.ts │ │ ├── combine.ts │ │ └── tests │ │ │ └── combine.test.ts │ ├── criteria.ts │ ├── criteria │ │ ├── chain.ts │ │ ├── char.ts │ │ ├── dash.ts │ │ ├── shift.ts │ │ ├── tests │ │ │ └── chain.test.ts │ │ ├── types.ts │ │ └── weight.ts │ ├── glyph.ts │ ├── lens.ts │ ├── registry.ts │ ├── relation.ts │ ├── relation │ │ ├── build.ts │ │ ├── counts.ts │ │ ├── create.ts │ │ ├── defs.ts │ │ ├── link.ts │ │ └── types.ts │ ├── tests │ │ └── glyph.test.ts │ └── types.ts ├── grid.ts ├── grid │ ├── alignGrid.ts │ ├── crop.ts │ ├── expand.ts │ ├── grid.ts │ ├── hAlignRow.ts │ ├── instances.ts │ ├── measure.ts │ ├── mod.ts │ ├── ops.ts │ ├── paint.ts │ ├── parse.ts │ ├── resize.ts │ ├── resizeElastic.ts │ ├── shrink.ts │ ├── stack.ts │ ├── stretch.ts │ ├── tests │ │ ├── alignGrid.test.ts │ │ ├── crop.test.ts │ │ ├── expand.test.ts │ │ ├── hAlignRow.test.ts │ │ ├── helpers.ts │ │ ├── instances.test.ts │ │ ├── measure.test.ts │ │ ├── ops.test.ts │ │ ├── paint.test.ts │ │ ├── parse.test.ts │ │ ├── resize.test.ts │ │ ├── shrink.test.ts │ │ ├── stack.test.ts │ │ ├── stackAlign.test.ts │ │ ├── stretch.test.ts │ │ ├── types.test.ts │ │ └── vAlignGrid.test.ts │ ├── types.ts │ └── vAlignGrid.ts ├── report.ts ├── report │ ├── chain.ts │ └── relation.ts ├── stacka.ts ├── style.ts ├── style │ ├── blend.ts │ ├── build.ts │ ├── deco.ts │ ├── instances.ts │ ├── lens.ts │ ├── ops.ts │ ├── style.ts │ ├── tests │ │ ├── blend.test.ts │ │ ├── build.test.ts │ │ ├── deco.test.ts │ │ ├── helpers.ts │ │ ├── instances.test.ts │ │ ├── lens.test.ts │ │ ├── ops.test.ts │ │ └── types.test.ts │ └── types.ts ├── term.ts ├── util │ ├── array.ts │ ├── chai.ts │ ├── fix │ │ ├── kind1.ts │ │ ├── kind2.ts │ │ └── tests │ │ │ ├── expr │ │ │ ├── eg.ts │ │ │ ├── ops.ts │ │ │ ├── schemes.ts │ │ │ └── types.ts │ │ │ └── kind1.test.ts │ ├── fp-ts.ts │ ├── function.ts │ ├── function │ │ ├── ops.ts │ │ ├── types.ts │ │ └── y.ts │ ├── lens.ts │ ├── number.ts │ ├── object.ts │ ├── object │ │ ├── get.ts │ │ ├── ops.ts │ │ ├── set.ts │ │ └── types.ts │ ├── string.ts │ ├── tco.ts │ ├── test.ts │ ├── tests │ │ └── tco.test.ts │ ├── trampoline │ │ ├── instances.ts │ │ ├── ops.ts │ │ ├── run.ts │ │ ├── tests │ │ │ ├── eg │ │ │ │ ├── common.ts │ │ │ │ ├── loop.ts │ │ │ │ ├── safe.ts │ │ │ │ └── unsafe.ts │ │ │ └── trampoline.test.ts │ │ └── types.ts │ ├── tree.ts │ ├── tree │ │ ├── build.ts │ │ ├── draw.ts │ │ ├── enumerate.ts │ │ ├── idTree.ts │ │ ├── instances.ts │ │ ├── lens.ts │ │ ├── ops.ts │ │ ├── schemes.ts │ │ ├── tests │ │ │ ├── TreeF.test.ts │ │ │ ├── draw.test.ts │ │ │ ├── enumerate.test.ts │ │ │ ├── idTree.test.ts │ │ │ ├── ops.test.ts │ │ │ └── schemes.test.ts │ │ ├── transform.ts │ │ └── types.ts │ ├── tuple.ts │ └── tuple │ │ ├── ops.ts │ │ └── types.ts └── vite-env.d.ts ├── test-data └── gold │ ├── eg │ ├── 1-hello-world.ts.out │ ├── 2-mondrian.ts.out │ ├── 3-waves.ts.out │ ├── 4-bar-chart.ts.out │ ├── 5-nested-borders.ts.out │ ├── 6-flow.ts.out │ ├── 7-table.ts.out │ ├── 8-logo.ts.out │ └── 9-blend-mode.ts.out │ └── reference │ ├── backdrops.ts.out │ ├── blends.ts.out │ ├── border │ ├── masks.ts.out │ └── sets.ts.out │ └── glyph │ ├── bitmaps.ts.out │ ├── next-glyph.ts.out │ └── relations.ts.out ├── tsconfig.build.json ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.json] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | README.md 3 | doc/ 4 | dist/ 5 | build/ 6 | coverage/ 7 | test-results/ 8 | test-data/ 9 | .eslintrc.cjs 10 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | env: { 5 | es6: true, 6 | node: true, 7 | commonjs: true, 8 | browser: true, 9 | }, 10 | 11 | plugins: ["@typescript-eslint", "prettier"], 12 | 13 | extends: [ 14 | "eslint:recommended", 15 | "plugin:prettier/recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | ], 18 | 19 | rules: { 20 | "@typescript-eslint/explicit-function-return-type": "off", 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/ban-types": "off", 23 | "@typescript-eslint/explicit-module-boundary-types": "off", 24 | "@typescript-eslint/no-empty-function": "off", 25 | "@typescript-eslint/no-empty-interface": "off", 26 | "arrow-parens": "off", 27 | "prettier/prettier": [ 28 | "error", 29 | { 30 | tabWidth: 2, 31 | singleQuote: true, 32 | trailingComma: "all", 33 | printWidth: 80, 34 | arrowParens: "avoid", 35 | endOfLine: "auto", 36 | }, 37 | ], 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /build/ 3 | /lib/ 4 | /dist/ 5 | /test-results/ 6 | /test-data/integration 7 | coverage 8 | *.log 9 | *-lock.yaml 10 | .vscode 11 | package-lock.json 12 | pnpm-lock.yaml 13 | .npmrc 14 | -------------------------------------------------------------------------------- /doc/images/box-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/images/box-types.png -------------------------------------------------------------------------------- /doc/images/color/chalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/images/color/chalk.png -------------------------------------------------------------------------------- /doc/images/color/lime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/images/color/lime.png -------------------------------------------------------------------------------- /doc/images/color/modded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/images/color/modded.png -------------------------------------------------------------------------------- /doc/reference/half-line-relations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/reference/half-line-relations.png -------------------------------------------------------------------------------- /doc/reference/line-characters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/reference/line-characters.png -------------------------------------------------------------------------------- /doc/reference/line-glyphs.json: -------------------------------------------------------------------------------- 1 | { 2 | "top": "▔", 3 | "right": "▕", 4 | "bottom": "▁", 5 | "left": "▏", 6 | "horizontal": "─", 7 | "vertical": "│", 8 | "thick": { 9 | "horizontal": "━", 10 | "vertical": "┃", 11 | "dotted": { 12 | "horizontal": "┉", 13 | "vertical": "┋" 14 | }, 15 | "dashed": { 16 | "horizontal": "┅", 17 | "vertical": "┇", 18 | "wide": { 19 | "horizontal": "╍", 20 | "vertical": "╏" 21 | } 22 | } 23 | }, 24 | "double": { 25 | "horizontal": "═", 26 | "vertical": "║" 27 | }, 28 | "dotted": { 29 | "horizontal": "┈", 30 | "vertical": "┊", 31 | "thick": { 32 | "horizontal": "┉", 33 | "vertical": "┋" 34 | } 35 | }, 36 | "dashed": { 37 | "horizontal": "┄", 38 | "vertical": "┆", 39 | "thick": { 40 | "horizontal": "┅", 41 | "vertical": "┇", 42 | "wide": { 43 | "horizontal": "╍", 44 | "vertical": "╏" 45 | } 46 | }, 47 | "wide": { 48 | "horizontal": "╌", 49 | "vertical": "╎", 50 | "thick": { 51 | "horizontal": "╍", 52 | "vertical": "╏" 53 | } 54 | } 55 | }, 56 | "near": { 57 | "top": "▔", 58 | "right": "▕", 59 | "bottom": "▁", 60 | "left": "▏" 61 | }, 62 | "halfSolid": { 63 | "top": "▀", 64 | "right": "▐", 65 | "bottom": "▄", 66 | "left": "▌" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /doc/reference/line-relations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/reference/line-relations.png -------------------------------------------------------------------------------- /doc/reference/named-border-sets.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/reference/named-border-sets.pdf -------------------------------------------------------------------------------- /doc/reference/named-border-sets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/reference/named-border-sets.png -------------------------------------------------------------------------------- /doc/reference/table-of-placement-combinators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/reference/table-of-placement-combinators.png -------------------------------------------------------------------------------- /doc/relations-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/relations-big.png -------------------------------------------------------------------------------- /doc/relations-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/relations-small.png -------------------------------------------------------------------------------- /doc/relations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/relations.png -------------------------------------------------------------------------------- /doc/screenshots/eg/1-hello-world.ts.alacritty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/1-hello-world.ts.alacritty.png -------------------------------------------------------------------------------- /doc/screenshots/eg/1-hello-world.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/1-hello-world.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/2-mondrian.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/2-mondrian.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/3-waves.ts.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/3-waves.ts.mkv -------------------------------------------------------------------------------- /doc/screenshots/eg/3-waves.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/3-waves.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/4-bar-chart.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/4-bar-chart.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/5-nested-borders.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/5-nested-borders.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/5-nested.borders.ts.alacritty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/5-nested.borders.ts.alacritty.png -------------------------------------------------------------------------------- /doc/screenshots/eg/6-flow.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/6-flow.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/7-table.ts.alacritty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/7-table.ts.alacritty.png -------------------------------------------------------------------------------- /doc/screenshots/eg/7-table.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/7-table.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/8-logo-alacritty.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/8-logo-alacritty.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/8-logo.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/8-logo.ts.png -------------------------------------------------------------------------------- /doc/screenshots/eg/9-blend-mode.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/eg/9-blend-mode.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/backdrops-kitty-fira-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/backdrops-kitty-fira-code.png -------------------------------------------------------------------------------- /doc/screenshots/reference/backdrops.ts.alacritty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/backdrops.ts.alacritty.png -------------------------------------------------------------------------------- /doc/screenshots/reference/backdrops.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/backdrops.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/blends.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/blends.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/border/masks.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/border/masks.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/border/sets.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/border/sets.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/glyph/another.next-glyph.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/glyph/another.next-glyph.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/glyph/bitmaps.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/glyph/bitmaps.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/glyph/next-glyph.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/glyph/next-glyph.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/glyph/relations.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/glyph/relations.ts.png -------------------------------------------------------------------------------- /doc/screenshots/reference/placements.ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middle-ages/stacka/76585fe4f17155fd6af5a47948850ba640b28de3/doc/screenshots/reference/placements.ts.png -------------------------------------------------------------------------------- /scripts/add-js-ext-to-dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Add .js file extension to all relative `require`s under the compile output 4 | # dir dist/ 5 | 6 | find dist -type f -exec sed -i -e "s/require('\\.\\(.\\+\\)');/require('\\.\\1.js'\\)/g" {} \; 7 | find dist -type f -exec sed -i -e 's/require("\.\(.\+\)");/require("\.\1.js"\)/g' {} \; 8 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Create a depenedncy free zero runtime Node.js 4 | # distribution ready to be run using the `node` 5 | # executable 6 | # E.g.: npm run build 7 | 8 | pm=`npm get package_manager`; 9 | 10 | pm=${pm:-npm} 11 | 12 | if [ "$pm" = "undefined" ]; then 13 | pm=npm 14 | fi 15 | 16 | set -Eeuo pipefail 17 | 18 | $pm build:compile 19 | $pm build:fix 20 | $pm build:types 21 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuo pipefail 4 | 5 | rm -rf dist \ 6 | test-results 7 | 8 | -------------------------------------------------------------------------------- /scripts/fix-dist-require.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Uses tsconfig.json path mappings to fix all `require`s under the compile 4 | # output dir dist. After this is run, `Node.js` should be able to run any 5 | # script in the project with no runtime requirements or resolver configuration. 6 | # 7 | # Fixes the following issues with tsc compiled Js output in a Node.js commonjs 8 | # project: 9 | # 10 | # 11 | # # 1. Apply tsconfig mappings 12 | # 13 | # Applies tsconfig.json path mappings so that mapped paths become relative 14 | # paths. For example assuming the tsconfig mapping: 15 | # 16 | # * `util/* ⇒ ./src/util/*`, 17 | # * `baseUrl = “.”` same dir as tsconfig.json, one above `src` 18 | # 19 | # Then a require of `util/foo` in a file `src/bar/baz.ts` will be replaced 20 | # with `../../util/foo`. 21 | # 22 | # 23 | # 24 | # # 2. Replace implicit index paths with explicit reference 25 | # 26 | # Replaces implicit index imports with explicit imports. For example, replaces 27 | # `./foo/bar`, where `bar` is a directory, with `./foo/bar/index` 28 | # 29 | # 30 | # 31 | # # 3. Adds `.js` extension to `require`s of relative imports 32 | # 33 | # For example replaces `./foo/bar/index` with `./foo/bar/index.js`. 34 | # 35 | # 36 | # Run using: 37 | # npm run build:node:fix 38 | 39 | pm=`npm get package_manager`; 40 | 41 | pm=${pm:-npm} 42 | 43 | if [ "$pm" = "undefined" ]; then 44 | pm=npm 45 | fi 46 | 47 | set -Eeuo pipefail 48 | 49 | $pm build:fix:require 50 | scripts/add-js-ext-to-dist.sh 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /scripts/record-examples.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | set -e 4 | 5 | # Run all example scripts and save their output to `test-data/integration` 6 | # directory. Useful for usage as a regression test oracle 7 | # 8 | # Example: npm run test:record-examples 9 | 10 | local dir="test-data/integration" 11 | mkdir -p "$dir/reference/border" "$dir/reference/glyph" "$dir/eg" 12 | 13 | echo "# Starting recording..."; 14 | 15 | export FORCE_COLOR=1 16 | 17 | time for n (${(f)}src/bin/(reference|eg)/**/*.ts) { tsx "$n" > "test-data/integration/${n:h:s/src\/bin\//}/${n:t}.out" } 18 | 19 | echo "# ✅ Done recording to “$dir/”"; 20 | 21 | -------------------------------------------------------------------------------- /scripts/run-ts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run a given typescript file 4 | # E.g.: npm run ts -- src/devBin/should-run.ts 5 | 6 | pm=`npm get package_manager`; 7 | 8 | pm=${pm:-npm} 9 | 10 | if [ "$pm" = "undefined" ]; then 11 | pm=npm 12 | fi 13 | 14 | source=${1:-src/devBin/should-run.ts} 15 | 16 | set -Eeuo pipefail 17 | 18 | shift 19 | 20 | ts-node-esm $source $@ 21 | -------------------------------------------------------------------------------- /scripts/test-repl-import.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Test that we can ESM import in the REPL 4 | # E.g.: npm run test:repl 5 | 6 | pm=`npm get package_manager`; 7 | 8 | pm=${pm:-npm} 9 | 10 | if [ "$pm" = "undefined" ]; then 11 | pm=npm 12 | fi 13 | 14 | echo 15 | echo "# Using package manager “$pm”" 16 | echo 17 | 18 | set -Eeuo pipefail 19 | 20 | import="import { toBinary } from 'src/devBin/pure'" 21 | program="'12d=' + toBinary(12) + 'b'" 22 | 23 | echo "$import;$program" | $pm run repl 24 | echo 25 | -------------------------------------------------------------------------------- /src/align.ts: -------------------------------------------------------------------------------- 1 | export * from './align/align'; 2 | -------------------------------------------------------------------------------- /src/align/lens.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import * as LE from 'monocle-ts/lib/Lens'; 3 | import { Lens } from 'monocle-ts/lib/Lens'; 4 | import { Align, HAlign, VAlign } from './types'; 5 | 6 | const id = LE.id(); 7 | 8 | export const hLens: Lens = FN.pipe(id, LE.prop('horizontal')), 9 | vLens: Lens = FN.pipe(id, LE.prop('vertical')); 10 | -------------------------------------------------------------------------------- /src/align/tests/instances.test.ts: -------------------------------------------------------------------------------- 1 | import { align } from 'src/align'; 2 | import { assert, suite, test } from 'vitest'; 3 | 4 | suite('instances', () => { 5 | test('show', () => assert.equal(align.show(align.topLeft), '⭶')); 6 | test('minSortedAlign', () => 7 | assert.equal( 8 | align.minSorted(align.topLeft, align.bottomRight), 9 | align.topLeft, 10 | )); 11 | }); 12 | -------------------------------------------------------------------------------- /src/backdrop.ts: -------------------------------------------------------------------------------- 1 | export * from './backdrop/backdrop'; 2 | -------------------------------------------------------------------------------- /src/backdrop/backdrop.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './instances'; 3 | export * from './paint'; 4 | export * from './named'; 5 | export * from './lens'; 6 | -------------------------------------------------------------------------------- /src/backdrop/instances.ts: -------------------------------------------------------------------------------- 1 | import { eq as EQ, show as SH } from 'fp-ts'; 2 | import * as GR from 'src/grid'; 3 | import { Backdrop } from './types'; 4 | 5 | export const show: SH.Show = { 6 | show: ({ image, project }) => 7 | `project:${project} using ${GR.show.show(image)}`, 8 | }; 9 | 10 | export const eq: EQ.Eq = { 11 | equals: ( 12 | { image: fstImage, project: fstProject }, 13 | { image: sndImage, project: sndProject }, 14 | ) => fstProject === sndProject && GR.eq.equals(fstImage, sndImage), 15 | }; 16 | -------------------------------------------------------------------------------- /src/backdrop/lens.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import * as GR from 'src/grid'; 3 | import * as LE from 'monocle-ts/lib/Lens'; 4 | import { Color } from 'src/color'; 5 | import * as grid from 'src/grid'; 6 | import { Style } from 'src/style'; 7 | import { Endo, Unary } from 'util/function'; 8 | import { modLens } from 'util/lens'; 9 | import { Pair } from 'util/tuple'; 10 | import { Backdrop } from './types'; 11 | 12 | const id = LE.id(); 13 | 14 | export type Mod = Unary>; 15 | 16 | export const image = FN.pipe(id, LE.prop('image'), modLens), 17 | project = FN.pipe(id, LE.prop('project'), modLens); 18 | 19 | export const modStyle: Mod> = FN.flow(grid.modStyle, image.mod), 20 | modRune: Mod> = FN.flow(grid.modRune, image.mod); 21 | 22 | export const setStyle: Unary> = FN.flow( 23 | FN.constant, 24 | modStyle, 25 | ), 26 | setRune: Unary> = FN.flow(FN.constant, modRune), 27 | [setFg, setBg]: Pair>> = [ 28 | FN.flow(grid.setFg, image.mod), 29 | FN.flow(grid.setBg, image.mod), 30 | ], 31 | colorBg = FN.flow(grid.colorBg, image.mod); 32 | 33 | export const [clearFg, clearBg, clearDeco, clearColor, flip] = [ 34 | image.mod(GR.clearFg), 35 | image.mod(GR.clearBg), 36 | image.mod(GR.clearDeco), 37 | image.mod(GR.clearColor), 38 | image.mod(GR.flip), 39 | ]; 40 | -------------------------------------------------------------------------------- /src/backdrop/paint.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { align } from 'src/align'; 3 | import { Size } from 'src/geometry'; 4 | import * as GR from 'src/grid'; 5 | import { Grid } from 'src/grid'; 6 | import { BinaryC } from 'util/function'; 7 | import { Backdrop, matchProjection } from './types'; 8 | 9 | /** 10 | * Paint a backdrop to a grid for the given size 11 | * 12 | * Used by `block.paint` to paint the block backdrop. 13 | */ 14 | export const paint: BinaryC = 15 | ({ image, project }) => 16 | size => 17 | GR.isEmpty(image) 18 | ? GR.empty() 19 | : FN.pipe( 20 | image, 21 | FN.pipe( 22 | size, 23 | FN.pipe( 24 | project, 25 | matchProjection( 26 | GR.resizeElastic, 27 | GR.repeat, 28 | GR.resize(align.middleCenter), 29 | ), 30 | ), 31 | ), 32 | ); 33 | 34 | export const asStrings: BinaryC = bd => 35 | FN.flow(paint(bd), GR.paint); 36 | 37 | export const asStringsWith = 38 | (filler: string): BinaryC => 39 | bd => 40 | FN.flow(paint(bd), GR.paintWith(filler)); 41 | -------------------------------------------------------------------------------- /src/backdrop/tests/lens.test.ts: -------------------------------------------------------------------------------- 1 | import { rgb } from 'ansis/colors'; 2 | import { function as FN } from 'fp-ts'; 3 | import { Size, size } from 'src/geometry'; 4 | import * as GR from 'src/grid'; 5 | import { head } from 'util/array'; 6 | import { Endo, Unary } from 'util/function'; 7 | import { assert, suite, test } from 'vitest'; 8 | import * as LE from '../lens'; 9 | import * as PA from '../paint'; 10 | import * as TY from '../types'; 11 | import { Backdrop } from '../types'; 12 | 13 | const withSquare = (f: Unary): R => FN.pipe(1, size.square, f); 14 | 15 | const printGrid = GR.paintWith('.'); 16 | 17 | suite('backdrop lens', () => { 18 | const blueOnRed = rgb(0, 0, 255).bgRgb(255, 0, 0); 19 | 20 | const iut: Backdrop = FN.pipe('A', blueOnRed, GR.parseRow, TY.center); 21 | 22 | const computeActual: Unary, string[]> = f => 23 | FN.pipe(iut, f, PA.paint, withSquare, printGrid); 24 | 25 | test('setFg', () => { 26 | const actual = FN.pipe(0xff_00_ff_ff, LE.setFg, computeActual, head); 27 | 28 | const expect = rgb(255, 255, 0).bgRgb(255, 0, 0)(`A`); 29 | 30 | assert.equal(actual, expect); 31 | }); 32 | 33 | test('setRune', () => 34 | assert.deepEqual( 35 | FN.pipe('B', LE.setRune, computeActual, head), 36 | blueOnRed('B'), 37 | )); 38 | }); 39 | -------------------------------------------------------------------------------- /src/backdrop/tests/repeat.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { flip } from 'fp-ts-std/Function'; 3 | import * as GR from 'src/grid'; 4 | import { Grid } from 'src/grid'; 5 | import { Binary } from 'util/function'; 6 | import { assert, suite, test } from 'vitest'; 7 | import { asStrings } from '../paint'; 8 | import { repeat } from '../types'; 9 | 10 | suite('backdrop repeat', () => { 11 | const image: Grid = GR.parseRows('left', ['AB', 'CD']); 12 | 13 | const apply: Binary = (width, height) => 14 | FN.pipe(image, repeat, FN.pipe({ width, height }, flip(asStrings))); 15 | 16 | test('repeat', () => 17 | assert.deepEqual(apply(4, 5), ['ABAB', 'CDCD', 'ABAB', 'CDCD', 'ABAB'])); 18 | 19 | test('crop', () => assert.deepEqual(apply(3, 1), ['ABA'])); 20 | }); 21 | -------------------------------------------------------------------------------- /src/bin/eg/1-hello-world.ts: -------------------------------------------------------------------------------- 1 | import { hex, magenta, cyan } from 'ansis/colors'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { border, box } from 'src/stacka'; 4 | 5 | pipe( 6 | [ 7 | hex('#bbb').italic`one boxed in`, 8 | magenta.bold`Hello 🌍 World`, 9 | hex('#888')('ANSI bold and'), 10 | cyan`brave` + ' ' + magenta.underline`magenta`, 11 | '+ one emoji', 12 | ], 13 | box.fromRows, 14 | box.center, 15 | box.hMargins(1), 16 | border.withFg('round', 'magenta'), 17 | box.print, 18 | ); 19 | -------------------------------------------------------------------------------- /src/bin/eg/2-mondrian.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { box, Box, Color, color } from 'src/stacka'; 3 | 4 | /* 5 | ┆ col23 ┆ 6 | ┄┄┄┄┄╴┌──────────┬───────────┐╶┄┄┄┄ 7 | │ A │ │ row1 8 | row12 ├──────────┤ D │╶┄┄┄┄ 9 | │ B │ │ row2 10 | ┄┄┄┄┄╴├──────────┼──────┬────┤╶┄┄┄┄ 11 | │ │ │ F │ row3 12 | row34 │ C │ E ├────┤╶┄┄┄┄ 13 | │ │ │ G │ row4 14 | ┄┄┄┄┄╴└──────────┴──────┴────┘╶┄┄┄┄ 15 | ┆ col1 ┆ col2 ┆col3┆ 16 | */ 17 | 18 | const [hGap, vGap] = [2, 1], 19 | [col1, col2, col3] = [5, 10, 6], 20 | [row1, row2, row3, row4] = [4, 4, 1, 2]; 21 | 22 | const col23 = col2 + col3 + hGap, 23 | row12 = row1 + row2 + vGap, 24 | row34 = row3 + row4 + vGap; 25 | 26 | const A = cell('A', [col1, row1], 'white'), 27 | B = cell('B', [col1, row2], 'white'), 28 | C = cell('C', [col1, row34], 'blue'), 29 | D = cell('D', [col23, row12], 'red'), 30 | E = cell('E', [col2, row34], 'white'), 31 | F = cell('F', [col3, row3], 'white'), 32 | G = cell('G', [col3, row4], 'yellow'); 33 | 34 | FN.pipe( 35 | FN.pipe([A, B, C], box.catBelowGap(vGap)), 36 | FN.pipe( 37 | G, 38 | FN.pipe(F, box.belowGap(vGap)), 39 | FN.pipe(E, box.rightOfGap(hGap)), 40 | FN.pipe(D, box.belowGap(vGap)), 41 | box.leftOfGap(hGap), 42 | ), 43 | box.print, 44 | ); 45 | 46 | function cell(key: string, [width, height]: [number, number], c: Color): Box { 47 | return box({ 48 | row: color.of(['black', c])(key), 49 | gridBg: c, 50 | width, 51 | height, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/bin/eg/5-nested-borders.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, tuple as TU } from 'fp-ts'; 2 | import { Border, border, box, Color, color } from 'src/stacka'; 3 | 4 | /** 5 | * Borders are implemented as boxes showing the border character at each 6 | * direction. Boxes nest, so border nest as well. 7 | */ 8 | 9 | const [sets, grays] = [border.sets, color.grays]; 10 | 11 | const borders: [Color, Border][] = [ 12 | [grays[87], sets.halfSolidNear], 13 | [grays[80], sets.halfSolidFar], 14 | [grays[74], sets.halfSolidNear], 15 | [grays[67], sets.space], 16 | [grays[55], sets.halfSolidNear], 17 | [grays[45], sets.space], 18 | [grays[30], sets.near], 19 | [grays[20], sets.line], 20 | [grays[10], sets.space], 21 | [grays[6], sets.dotted], 22 | ]; 23 | 24 | // a list of borders to be nested 25 | const colored: Border[] = FN.pipe( 26 | borders, 27 | AR.mapWithIndex((idx, [clr, br]) => 28 | FN.pipe( 29 | br, 30 | FN.pipe( 31 | [clr, 'black'] as [Color, Color], 32 | idx ? FN.identity : TU.swap, 33 | border.setColor, 34 | ), 35 | ), 36 | ), 37 | ); 38 | 39 | FN.pipe( 40 | 'borders nest', 41 | color.fg('darkblue'), 42 | box.of, 43 | box.colorBg('white'), 44 | border.colored('near', [grays[97], grays[92]]), 45 | border.nest(colored), 46 | box.print, 47 | ); 48 | -------------------------------------------------------------------------------- /src/bin/eg/6-flow.ts: -------------------------------------------------------------------------------- 1 | import { nonEmptyArray as NEA } from 'fp-ts'; 2 | import { flow, pipe } from 'fp-ts/function'; 3 | import { block, border, Box, box, boxes, color } from 'src/stacka'; 4 | 5 | /** 6 | * Demo of `boxes.flow` and marking clipped boxes 7 | * 8 | * 10 boxes are drawn, each wider than the previous, starting at `width=5` and 9 | * ending at `width=32`, with jumps of `3` between widths. These are the 10 | * _parents_. 11 | * 12 | * Inside each parent 10 child boxes are drawn. Again each is wider than the 13 | * previous, ranging from 1 to 10. 14 | * 15 | * The parents are _flow_ boxes, so child boxes will flow if they don't fit. 16 | * 17 | * Sometimes the child box cannot fit the parent _at all_, even if we start a 18 | * new row. We use to the `clipMark` field of the `FlowConfig` to highlight 19 | * these boxes. 20 | */ 21 | 22 | const nDigits = (n: number): string => 23 | pipe( 24 | NEA.range(0, n) 25 | .map(s => s.toString()) 26 | .join(''), 27 | color.fg(0xff_a0_90_60), 28 | ); 29 | 30 | const child: (idx: number) => Box = flow(nDigits, box.fromRow, border.hMcGugan); 31 | 32 | const parent = (width: number): Box => 33 | pipe( 34 | NEA.range(0, 9), 35 | NEA.map(child), 36 | boxes.flow.of({ 37 | placeH: box.catRightOfTop, 38 | placeV: box.catBelow, 39 | clipMark: pipe('darkred', block.setGridFg, box.mapBlock), 40 | available: 3 * width + 2, 41 | }), 42 | border.withFg('line', 'darkgray'), 43 | ); 44 | 45 | pipe( 46 | NEA.range(1, 10), 47 | NEA.map(parent), 48 | boxes.win.flow.of({ 49 | placeH: box.catSnugRightOfTop, 50 | placeV: box.catSnugBelow, 51 | hGap: -1, 52 | }), 53 | box.print, 54 | ); 55 | -------------------------------------------------------------------------------- /src/bin/reference/border/sets.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, tuple as TU } from 'fp-ts'; 2 | import { Border, border, BorderName, Box, box, Color, color } from 'src/stacka'; 3 | import stringWidth from 'string-width'; 4 | import * as HE from '../../helpers'; 5 | 6 | /** Report on all named border sets */ 7 | 8 | const borders = Object.entries(border.sets) as [BorderName, Border][]; 9 | 10 | const demo: [Color, Color] = ['lime', 'black']; 11 | 12 | const cellWidth = 13 | Math.max(...FN.pipe(borders, AR.map(FN.flow(TU.fst, stringWidth)))) + 1; 14 | 15 | const demoBox = ([name, br]: [string, Border]): Box => 16 | box.centered({ 17 | width: cellWidth, 18 | row: FN.pipe(name, color.fg(HE.lighten('lime'))), 19 | apply: FN.pipe(br, border.setColor(demo), border), 20 | gridBg: HE.grays[10], 21 | }); 22 | 23 | const gallery = HE.colorGallery([1, 1])('Named Border Sets'); 24 | 25 | FN.pipe(borders, AR.map(demoBox), gallery, box.print); 26 | -------------------------------------------------------------------------------- /src/bin/reference/glyph/bitmaps.ts: -------------------------------------------------------------------------------- 1 | import { termSize, bitmap, BitmapRole } from 'src/stacka'; 2 | 3 | /** 4 | * With no arguments, shows all character bitmaps in every box drawing character 5 | * role. 6 | * 7 | * If given a command separated list of such roles, they will be the only roles shown. 8 | * 9 | * If given `-h` shows the names of all roles. 10 | * 11 | * Example: 12 | * 13 | * ```txt 14 | * bitmaps.ts elbow,hLine 15 | * ``` 16 | */ 17 | 18 | const allRoles = bitmap.roles; 19 | 20 | if (process.argv[2] === '-h') { 21 | console.log('All box drawing character roles: ' + allRoles.join(',')); 22 | process.exit(); 23 | } 24 | 25 | const [width] = termSize(); 26 | 27 | const roles = 28 | process.argv[2] !== undefined 29 | ? (process.argv[2].split(',') as BitmapRole[]) 30 | : allRoles; 31 | 32 | const report = bitmap.rolesReport(width)(roles); 33 | 34 | console.log(report); 35 | -------------------------------------------------------------------------------- /src/bin/reference/glyph/relations.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { unlines } from 'fp-ts-std/String'; 3 | import { glyph, RelationName, relationReport, termWidth } from 'src/stacka'; 4 | 5 | /** 6 | * With no arguments, shows all relations found between the line drawing 7 | * characters. 8 | * 9 | * If given a command separated list of relation names, they will be the only 10 | * relations shown. 11 | * 12 | * `-h` will show the names of all relations. 13 | * 14 | * Example: 15 | * 16 | * ```txt 17 | * relations.ts invert,weight 18 | * ``` 19 | */ 20 | const arg = process.argv[2]; 21 | 22 | const names = glyph.allRelationNames; 23 | 24 | if (arg === '-h') { 25 | console.log('All box drawing character relations:'); 26 | console.log(names); 27 | process.exit(); 28 | } 29 | 30 | const relations = 31 | arg !== undefined 32 | ? (arg.split(',') as RelationName[]) 33 | : glyph.allRelationNames, 34 | makeReport = relationReport(termWidth()), 35 | report = (idx: number, name: RelationName): string => makeReport([idx, name]), 36 | reports = FN.pipe(relations, AR.mapWithIndex(report), unlines); 37 | 38 | console.log(reports); 39 | -------------------------------------------------------------------------------- /src/bitmap.ts: -------------------------------------------------------------------------------- 1 | export * from './bitmap/bitmap'; 2 | -------------------------------------------------------------------------------- /src/bitmap/bitmap.ts: -------------------------------------------------------------------------------- 1 | import { roleReport, rolesReport } from './rolesReport'; 2 | import { registry } from './registry'; 3 | import * as quadRes from './quadRes'; 4 | import * as role from './role'; 5 | import * as data from './data'; 6 | import * as ops from './ops'; 7 | import { named } from './named'; 8 | 9 | export type { 10 | Px, 11 | PxRow, 12 | Matrix, 13 | MatrixRow, 14 | TupleRes, 15 | RowOp, 16 | RowCheck, 17 | Check, 18 | RelCheck, 19 | } from './data'; 20 | 21 | export type { BitmapRole } from './role'; 22 | export type { Registry } from './registry'; 23 | export type { BasicGroup, ElbowGroup, LineGroup } from './named'; 24 | 25 | export const bitmap = { 26 | ...role, 27 | ...data, 28 | ...ops, 29 | ...quadRes, 30 | ...registry, 31 | ...named, 32 | registry: registry.reg, 33 | roleReport, 34 | rolesReport, 35 | } as const; 36 | -------------------------------------------------------------------------------- /src/bitmap/data.ts: -------------------------------------------------------------------------------- 1 | export * from './data/types'; 2 | export * from './data/parse'; 3 | -------------------------------------------------------------------------------- /src/bitmap/data/cross.ts: -------------------------------------------------------------------------------- 1 | export const cross = ` 2 | ┼ ╋ ╬ ┿ ╂ ╪ ╫ 3 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺#⁺⁺#⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺#⁺⁺#⁺⁺ 4 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺#⁺⁺#⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺#⁺⁺#⁺⁺ 5 | ⁺⁺⁺##⁺⁺⁺ ######## ###⁺⁺### ######## ⁺⁺####⁺⁺ ######## ⁺⁺#⁺⁺#⁺⁺ 6 | ######## ######## ⁺⁺⁺⁺⁺⁺⁺⁺ ######## ######## ⁺⁺⁺##⁺⁺⁺ ######## 7 | ######## ######## ⁺⁺⁺⁺⁺⁺⁺⁺ ######## ######## ⁺⁺⁺##⁺⁺⁺ ######## 8 | ⁺⁺⁺##⁺⁺⁺ ######## ###⁺⁺### ######## ⁺⁺####⁺⁺ ######## ⁺⁺#⁺⁺#⁺⁺ 9 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺#⁺⁺#⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺#⁺⁺#⁺⁺ 10 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺#⁺⁺#⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺#⁺⁺#⁺⁺ 11 | ╀ ┾ ╁ ┽ ╈ ╉ ╇ ╊ 12 | ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ 13 | ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ 14 | ⁺⁺####⁺⁺ ⁺⁺⁺##### ⁺⁺⁺##⁺⁺⁺ #####⁺⁺⁺ ######## ######⁺⁺ ######## ⁺⁺###### 15 | ######## ######## ######## ######## ######## ######## ######## ######## 16 | ######## ######## ######## ######## ######## ######## ######## ######## 17 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##### ⁺⁺####⁺⁺ #####⁺⁺⁺ ######## ######⁺⁺ ######## ⁺⁺###### 18 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ 19 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ 20 | ╄ ╃ ╆ ╅ + 21 | ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺ 22 | ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ 23 | ⁺⁺###### ######⁺⁺ ⁺⁺###### ######⁺⁺ ⁺⁺⁺##⁺⁺⁺ 24 | ######## ######## ######## ######## ⁺######⁺ 25 | ######## ######## ######## ######## ⁺######⁺ 26 | ⁺⁺###### ######⁺⁺ ⁺⁺###### ######⁺⁺ ⁺⁺⁺##⁺⁺⁺ 27 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺##⁺⁺⁺ 28 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ ⁺⁺####⁺⁺ ⁺⁺####⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺`; 29 | -------------------------------------------------------------------------------- /src/bitmap/data/halftone.ts: -------------------------------------------------------------------------------- 1 | export const halftone = ` 2 | ␠ ░ ▒ ▓ █ 3 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺#⁺#⁺#⁺# ######## ######## 4 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺#⁺#⁺#⁺# #⁺#⁺#⁺#⁺ #⁺#⁺#⁺#⁺ ######## 5 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺#⁺#⁺#⁺# ######## ######## 6 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺#⁺#⁺#⁺# #⁺#⁺#⁺#⁺ #⁺#⁺#⁺#⁺ ######## 7 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺#⁺#⁺#⁺# ######## ######## 8 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺#⁺#⁺#⁺# #⁺#⁺#⁺#⁺ #⁺#⁺#⁺#⁺ ######## 9 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺#⁺#⁺#⁺# ######## ######## 10 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺#⁺#⁺#⁺# #⁺#⁺#⁺#⁺ #⁺#⁺#⁺#⁺ ########`; 11 | -------------------------------------------------------------------------------- /src/bitmap/data/parse.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { transpose } from 'fp-ts-std/Array'; 3 | import { tail } from 'util/array'; 4 | import { Unary } from 'util/function'; 5 | import { lines, split } from 'util/string'; 6 | import { Matrix, Px, PxRow, resolution } from './types'; 7 | 8 | const linesPerRow = resolution + 1; // line with actual glyphs 9 | 10 | const parsePx: Unary = char => (char === '#' ? '#' : '⁺'), 11 | parseLine = (line: string) => 12 | FN.pipe(line, Array.from, AR.map(parsePx)) as PxRow; 13 | 14 | export const parseDef = (lines: string[]) => 15 | FN.pipe(lines, AR.map(parseLine)) as Matrix; 16 | 17 | const parseRow: Unary = ([ 18 | labelLine, 19 | ...bitmapLines 20 | ]) => { 21 | const labels = FN.pipe( 22 | labelLine, 23 | split(/\s+/), 24 | AR.filter(s => s.length > 0), 25 | AR.map(s => s.replace('␠', ' ')), 26 | ), 27 | matrices = FN.pipe( 28 | bitmapLines, 29 | AR.map(split(/\s/)), 30 | transpose, 31 | AR.map(parseDef), 32 | ); 33 | 34 | return FN.pipe(labels, AR.zip(matrices)); 35 | }; 36 | 37 | export const parse: Unary = FN.flow( 38 | lines, 39 | tail, 40 | AR.chunksOf(linesPerRow), 41 | AR.chain(parseRow), 42 | ); 43 | -------------------------------------------------------------------------------- /src/bitmap/data/tests/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import { parseDef } from '../parse'; 4 | import { Matrix, resolution, toBin } from '../types'; 5 | 6 | const matrix: Matrix = parseDef([ 7 | '########', 8 | '########', 9 | '########', 10 | '########', 11 | '########', 12 | '########', 13 | '########', 14 | '########', 15 | ]); 16 | 17 | suite('bitmap data parse', () => { 18 | test('basic', () => 19 | assert.equal( 20 | toBin(matrix), 21 | AR.replicate(resolution * resolution, '1').join(''), 22 | )); 23 | }); 24 | -------------------------------------------------------------------------------- /src/bitmap/data/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | array as AR, 3 | function as FN, 4 | nonEmptyArray as NE, 5 | predicate as PRE, 6 | } from 'fp-ts'; 7 | import { stringEq } from 'util/string'; 8 | import { BinOp, Endo, Unary } from 'util/function'; 9 | import { Pair } from 'util/tuple'; 10 | import { mapBoth } from 'fp-ts-std/Tuple'; 11 | 12 | export type Px = '#' | '⁺'; 13 | export type PxRow = TupleRes; 14 | export type Matrix = TupleRes; 15 | export type MatrixRow = `${Px}${Px}${Px}${Px}${Px}${Px}${Px}${Px}`; 16 | export type TupleRes = [T, T, T, T, T, T, T, T]; 17 | 18 | export type RowOp = BinOp; 19 | export type RowCheck = PRE.Predicate; 20 | export type Check = PRE.Predicate; 21 | export type RelCheck = PRE.Predicate>; 22 | 23 | export const resolution = 8, 24 | resolutionRange = NE.range(0, resolution - 1), 25 | halfRes = Math.floor(resolution / 2), 26 | thirdRes = Math.floor(resolution / 3), 27 | thirdResRange = NE.range(0, thirdRes - 1); 28 | 29 | export const [pxOn, pxOff]: Pair = ['#', '⁺']; 30 | 31 | export const [emptyRow, fullRow]: Pair = [ 32 | ['⁺', '⁺', '⁺', '⁺', '⁺', '⁺', '⁺', '⁺'], 33 | ['#', '#', '#', '#', '#', '#', '#', '#'], 34 | ], 35 | [emptyMatrix, fullMatrix]: Pair = FN.pipe( 36 | [emptyRow, fullRow], 37 | mapBoth((t: T) => AR.replicate(resolution, t) as TupleRes), 38 | ); 39 | 40 | export const isOn: PRE.Predicate = stringEq('#'), 41 | isOff: PRE.Predicate = PRE.not(isOn), 42 | invertPx: Endo = px => (isOn(px) ? pxOff : pxOn); 43 | 44 | export const toBin: Unary = bm => 45 | FN.pipe(bm, AR.chain(AR.map(p => (isOn(p) ? '1' : '0')))).join(''); 46 | -------------------------------------------------------------------------------- /src/bitmap/named.ts: -------------------------------------------------------------------------------- 1 | import { line } from './named/line'; 2 | import * as elbow from './named/elbow'; 3 | import * as classify from './named/classify'; 4 | import * as other from './named/other'; 5 | import * as types from './named/types'; 6 | 7 | export type { BasicGroup, ElbowGroup, LineGroup } from './named/types'; 8 | 9 | export const named = { 10 | line, 11 | dash: line.dash, 12 | ...elbow, 13 | ...classify, 14 | ...elbow, 15 | ...other, 16 | ...types, 17 | } as const; 18 | -------------------------------------------------------------------------------- /src/bitmap/named/line.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { array as AR, function as FN } from 'fp-ts'; 3 | import { dir, Direct, Orient } from 'src/geometry'; 4 | import { Unary } from 'util/function'; 5 | import { solid, space } from './other'; 6 | 7 | const direct: Unary = quad => { 8 | assert(quad.length === 4, `quad with length≠4: ${quad}`); 9 | const [top, right, bottom, left] = Array.from(quad); 10 | return { top, right, bottom, left }; 11 | }; 12 | 13 | const orient: Unary = pair => { 14 | assert(pair.length === 2, `orient with length≠2: ${pair}`); 15 | const [horizontal, vertical] = Array.from(pair); 16 | return { horizontal, vertical }; 17 | }; 18 | 19 | const mono = dir.singleton; 20 | 21 | const [lineDash, dot, wide, thickDash, thickDot, thickWide] = FN.pipe( 22 | ['┄┆', '┈┊', '╌╎', '┅┇', '┉┋', '╍╏'], 23 | AR.map(orient), 24 | ); 25 | 26 | const dashed = { 27 | ...lineDash, 28 | thick: { ...thickDash, wide: thickWide }, 29 | } as const, 30 | dotted = { ...dot, thick: thickDot } as const, 31 | dashedWide = { ...wide, thick: thickWide } as const, 32 | dash = { ...dashed, dot: dotted, wide: dashedWide } as const, 33 | thick = { 34 | ...orient('━┃'), 35 | dash: thickDash, 36 | dot: thickDot, 37 | wide: thickWide, 38 | } as const; 39 | 40 | export const line = { 41 | ...direct('▔▕▁▏'), 42 | ...orient('─│'), 43 | thick, 44 | dash, 45 | double: orient('═║'), 46 | halfSolid: direct('▀▐▄▌'), 47 | space: mono(space), 48 | solid: mono(solid), 49 | } as const; 50 | -------------------------------------------------------------------------------- /src/bitmap/named/other.ts: -------------------------------------------------------------------------------- 1 | export const tee = { 2 | top: '┴', 3 | right: '├', 4 | bottom: '┬', 5 | left: '┤', 6 | thick: { 7 | top: '┻', 8 | right: '┣', 9 | bottom: '┳', 10 | left: '┫', 11 | }, 12 | double: { 13 | top: '╩', 14 | right: '╠', 15 | bottom: '╦', 16 | left: '╣', 17 | }, 18 | } as const; 19 | 20 | export const cross = '┼', 21 | space = ' ' as const, 22 | solid = '█' as const; 23 | -------------------------------------------------------------------------------- /src/bitmap/ops.ts: -------------------------------------------------------------------------------- 1 | export * as row from './ops/row'; 2 | export * as query from './ops/query'; 3 | export * as switches from './ops/switches'; 4 | export * from './ops/matrix'; 5 | export * from './ops/stack'; 6 | export * from './ops/instances'; 7 | -------------------------------------------------------------------------------- /src/bitmap/ops/instances.ts: -------------------------------------------------------------------------------- 1 | import { 2 | array as AR, 3 | eq as EQ, 4 | function as FN, 5 | monoid as MO, 6 | show as SH, 7 | string as STR, 8 | } from 'fp-ts'; 9 | import { curry2 } from 'fp-ts-std/Function'; 10 | import { unlines } from 'fp-ts-std/String'; 11 | import { mapBoth } from 'fp-ts-std/Tuple'; 12 | import { Pair } from 'util/tuple'; 13 | import { 14 | Check, 15 | emptyMatrix, 16 | emptyRow, 17 | fullMatrix, 18 | fullRow, 19 | Matrix, 20 | Px, 21 | PxRow, 22 | RowCheck, 23 | } from '../data/types'; 24 | import * as row from './row'; 25 | 26 | const pxMonoid: MO.Monoid = { 27 | empty: '⁺', 28 | concat: (fst, snd) => (fst === snd ? fst : '#'), 29 | }; 30 | 31 | export const pxRowMonoid: MO.Monoid = { 32 | empty: emptyRow, 33 | concat: (fst, snd) => AR.zipWith(fst, snd, pxMonoid.concat) as PxRow, 34 | }; 35 | 36 | export const monoid: MO.Monoid = { 37 | empty: emptyMatrix, 38 | concat: (fst, snd) => AR.zipWith(fst, snd, pxRowMonoid.concat) as Matrix, 39 | }; 40 | 41 | export const pxEq: EQ.Eq = STR.Eq, 42 | pxRowEq: EQ.Eq = AR.getEq(pxEq), 43 | matrixEq: EQ.Eq = AR.getEq(pxRowEq); 44 | 45 | export const [isEmptyRow, isFullRow]: Pair = [ 46 | curry2(pxRowEq.equals)(emptyRow), 47 | curry2(pxRowEq.equals)(fullRow), 48 | ]; 49 | 50 | export const showPxRow: SH.Show = { show: row.joinPx }; 51 | 52 | export const showMatrix: SH.Show = { 53 | show: FN.flow(AR.map(row.joinPx), unlines), 54 | }; 55 | 56 | export const [isEmpty, isFull]: Pair = FN.pipe( 57 | [fullMatrix, fullMatrix], 58 | FN.pipe(matrixEq.equals, curry2, mapBoth), 59 | ); 60 | -------------------------------------------------------------------------------- /src/bitmap/ops/query.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, tuple as TU } from 'fp-ts'; 2 | import { Endo, Unary } from 'util/function'; 3 | import { Matrix, RelCheck } from '../data'; 4 | import * as IN from './instances'; 5 | import * as matrix from './matrix'; 6 | 7 | const matEq = FN.tupled(IN.matrixEq.equals), 8 | fromOp: Unary, RelCheck> = f => FN.flow(TU.mapSnd(f), matEq); 9 | 10 | /** When first is inverted, it is equal to the second */ 11 | export const invertEq: RelCheck = fromOp(matrix.invert); 12 | 13 | /** When first is flipped on vertical axis, it is equal to the second */ 14 | export const hFlipEq: RelCheck = fromOp(matrix.hFlip); 15 | 16 | /** When first is flipped on horizontal axis, it is equal to the second */ 17 | export const vFlipEq: RelCheck = fromOp(matrix.vFlip); 18 | 19 | /** When first is turned 90ᵒ clockwise, it is equal to the second */ 20 | export const turnEq: RelCheck = fromOp(matrix.turn); 21 | -------------------------------------------------------------------------------- /src/bitmap/ops/row.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, predicate as PRE } from 'fp-ts'; 2 | import { join, sum } from 'fp-ts-std/Array'; 3 | import { splitAt } from 'util/array'; 4 | import { BinaryC, Endo, Unary } from 'util/function'; 5 | import { stringEq } from 'util/string'; 6 | import { invertPx, isOn, MatrixRow, Px, PxRow, TupleRes } from '../data/types'; 7 | 8 | export const [splitPx, joinPx]: [ 9 | Unary, 10 | Unary, 11 | ] = [row => Array.from(row) as TupleRes, row => row.join('') as MatrixRow]; 12 | 13 | export const invert: Endo = row => 14 | FN.pipe(row, AR.map(invertPx)) as PxRow; 15 | 16 | export const isOnAt: Unary> = row => left => 17 | isOn(row[left]); 18 | 19 | export const mod: BinaryC, number, Endo> = f => i => row => { 20 | const [before, px, after] = FN.pipe(row, splitAt(i)); 21 | return [...before, f(px), ...after] as PxRow; 22 | }; 23 | 24 | export const countPx: Unary = row => 25 | FN.pipe( 26 | row, 27 | AR.map(px => (isOn(px) ? 1 : 0)), 28 | sum, 29 | ); 30 | 31 | export const countAt: BinaryC = idxs => row => 32 | FN.pipe( 33 | idxs, 34 | FN.pipe(row, isOnAt, AR.map), 35 | AR.map(t => (t ? 1 : 0)), 36 | sum, 37 | ); 38 | 39 | export const flip: Endo = px => FN.pipe(px, AR.reverse) as PxRow; 40 | 41 | export const isSymmetric: PRE.Predicate = row => 42 | FN.pipe(row, AR.reverse, join(''), FN.pipe(row, join(''), stringEq)); 43 | -------------------------------------------------------------------------------- /src/bitmap/ops/stack.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, monoid as MO, option as OP } from 'fp-ts'; 2 | import { Pair } from 'util/tuple'; 3 | import { Unary } from 'util/function'; 4 | import { Matrix } from '../data'; 5 | import * as switches from './switches'; 6 | import { registry as reg } from '../registry'; 7 | import * as IN from './instances'; 8 | 9 | /** 10 | * Given a set of `Bitmap⇒Bitmap` functions, and a pair of bitmaps, for each 11 | * function, run it on the bitmap and return a character if found. If no 12 | * characters found for any of the functions, returns `None`. 13 | */ 14 | export const tryStacks: Unary, OP.Option> = bms => { 15 | const stacked = FN.pipe(bms, MO.concatAll(IN.monoid)); 16 | 17 | for (const pixelSwitch of switches.centered) { 18 | const res = FN.pipe(stacked, pixelSwitch, reg.charByMatrix); 19 | 20 | if (OP.isSome(res)) return res; 21 | } 22 | return OP.none; 23 | }; 24 | -------------------------------------------------------------------------------- /src/bitmap/ops/switches.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { cartesian } from 'fp-ts-std/Array'; 3 | import { applyEvery, flip } from 'fp-ts-std/Function'; 4 | import { mapBoth } from 'fp-ts-std/Tuple'; 5 | import { Pair } from 'util/tuple'; 6 | import { Endo, Unary } from 'util/function'; 7 | import { Matrix } from '../data'; 8 | import * as matrix from './matrix'; 9 | 10 | const off: Unary[], Endo> = FN.flow( 11 | AR.map(matrix.setPxOff), 12 | applyEvery, 13 | ); 14 | 15 | const centerRowsOff: Unary> = FN.flow( 16 | flip[]>(cartesian)([3, 4]), 17 | off, 18 | ); 19 | 20 | export const [left, center, right] = FN.pipe( 21 | [[2], [3, 4], [5]], 22 | AR.map(centerRowsOff), 23 | ), 24 | [bottom, top] = FN.pipe( 25 | [right, left], 26 | mapBoth(f => FN.flow(matrix.turn, f, matrix.antiTurn)), 27 | ); 28 | 29 | const threeDirections: Endo[] = [ 30 | FN.flow(top, right, bottom), 31 | FN.flow(right, bottom, left), 32 | FN.flow(bottom, left, top), 33 | FN.flow(left, top, right), 34 | ]; 35 | 36 | export const thinThickFix: Endo[] = FN.pipe( 37 | [ 38 | [2, 2], 39 | [2, 5], 40 | [5, 2], 41 | [5, 5], 42 | ], 43 | AR.map(matrix.setPxOn), 44 | ); 45 | 46 | export const centered: Endo[] = [ 47 | FN.identity, 48 | ...thinThickFix, 49 | center, 50 | ...threeDirections, 51 | FN.flow(threeDirections[0], left), 52 | ]; 53 | 54 | /* 55 | 56 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ 57 | ⁺⁺⁺##⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ 58 | #####⁺⁺⁺ ⁺⁺⁺##⁺⁺⁺ 59 | ######## ⁺⁺⁺##### 60 | ######## ⁺⁺⁺##### 61 | #####⁺⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺ 62 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺ 63 | ⁺⁺⁺⁺⁺⁺⁺⁺ ⁺⁺⁺⁺⁺⁺⁺⁺ 64 | 65 | */ 66 | -------------------------------------------------------------------------------- /src/bitmap/ops/tests/matrix.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import { showMatrix } from '../instances'; 4 | import * as matrix from '../matrix'; 5 | import { 6 | fullMatrix, 7 | halfRes, 8 | resolution, 9 | fullRow, 10 | pxOn, 11 | PxRow, 12 | pxOff, 13 | Matrix, 14 | } from '../../data'; 15 | 16 | suite('bitmap matrix ops', () => { 17 | test('invertPxAt', () => { 18 | const actual = FN.pipe( 19 | fullMatrix, 20 | matrix.invertPxAt([0, 3]), 21 | matrix.invertPxAt([0, 4]), 22 | showMatrix.show, 23 | ); 24 | 25 | const expect = showMatrix.show([ 26 | [ 27 | ...AR.replicate(halfRes - 1, pxOn), 28 | pxOff, 29 | pxOff, 30 | ...AR.replicate(halfRes - 1, pxOn), 31 | ] as PxRow, 32 | ...AR.replicate(resolution - 1, fullRow), 33 | ] as Matrix); 34 | 35 | assert.deepEqual(actual, expect); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/bitmap/ops/tests/query.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import { emptyMatrix, fullMatrix } from '../../data'; 4 | import * as query from '../query'; 5 | 6 | suite('bitmap query ops', () => { 7 | suite('invertEq', () => { 8 | test('full→empty', () => 9 | FN.pipe([fullMatrix, emptyMatrix], query.invertEq, assert.isTrue)); 10 | test('empty→full', () => 11 | FN.pipe([emptyMatrix, fullMatrix], query.invertEq, assert.isTrue)); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/bitmap/ops/tests/row.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import * as row from '../row'; 4 | import { 5 | resolution, 6 | emptyRow, 7 | fullRow, 8 | pxOff, 9 | resolutionRange, 10 | } from '../../data'; 11 | 12 | suite('bitmap row ops', () => { 13 | suite('invert', () => { 14 | test('full', () => assert.deepEqual(row.invert(fullRow), emptyRow)); 15 | test('empty', () => assert.deepEqual(row.invert(emptyRow), fullRow)); 16 | }); 17 | 18 | suite('countAt', () => { 19 | const countAll = row.countAt(resolutionRange); 20 | test('full', () => assert.equal(countAll(fullRow), resolution)); 21 | test('empty', () => assert.equal(countAll(emptyRow), 0)); 22 | }); 23 | 24 | suite('isSymmetric', () => { 25 | test('full ⊤', () => assert.isTrue(row.isSymmetric(emptyRow))); 26 | test('empty ⊤', () => assert.isTrue(row.isSymmetric(emptyRow))); 27 | test('asymetric ⊥', () => 28 | test('asymetric', () => 29 | FN.pipe( 30 | fullRow, 31 | FN.pipe(2, FN.pipe(pxOff, FN.constant, row.mod)), 32 | row.isSymmetric, 33 | assert.isFalse, 34 | ))); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/bitmap/ops/tests/switches.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import * as data from '../../data'; 4 | import * as switches from '../switches'; 5 | import { Matrix, PxRow } from '../../data/types'; 6 | import { Endo } from 'util/function'; 7 | import { dup } from 'fp-ts-std/Tuple'; 8 | import { showMatrix } from '../instances'; 9 | 10 | const show = showMatrix.show; 11 | 12 | const check = (name: string, sw: Endo, expect: Matrix) => 13 | test(name, () => assert.deepEqual(show(sw(data.fullMatrix)), show(expect))); 14 | 15 | suite('bitmap switch ops', () => { 16 | check('left', switches.left, [ 17 | ...AR.replicate(3, data.fullRow), 18 | ...dup(Array.from('##⁺#####') as PxRow), 19 | ...AR.replicate(3, data.fullRow), 20 | ] as Matrix); 21 | 22 | check('right', switches.right, [ 23 | ...AR.replicate(3, data.fullRow), 24 | ...dup(Array.from('#####⁺##') as PxRow), 25 | ...AR.replicate(3, data.fullRow), 26 | ] as Matrix); 27 | 28 | check('top', switches.top, [ 29 | ...AR.replicate(2, data.fullRow), 30 | Array.from('###⁺⁺###') as PxRow, 31 | ...AR.replicate(5, data.fullRow), 32 | ] as Matrix); 33 | 34 | check('bottom', switches.bottom, [ 35 | ...AR.replicate(5, data.fullRow), 36 | Array.from('###⁺⁺###') as PxRow, 37 | ...AR.replicate(2, data.fullRow), 38 | ] as Matrix); 39 | }); 40 | -------------------------------------------------------------------------------- /src/bitmap/quadRes.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, option as OP } from 'fp-ts'; 2 | import { join } from 'fp-ts-std/Array'; 3 | import { chunk4x } from 'util/array'; 4 | import { BinaryC, Endo, Unary } from 'util/function'; 5 | import { flattenPair, Pair } from 'util/tuple'; 6 | import { Matrix, Px } from './data'; 7 | 8 | /** Show a matrix more efficiently using half-character pixels */ 9 | export const quadResWith: BinaryC, Matrix, string[]> = 10 | pixelColor => bm => { 11 | const foldPx = OP.fold( 12 | () => 'F', 13 | px => (px === '#' ? 'T' : 'F'), 14 | ); 15 | 16 | const quadToGlyph = { 17 | FFFF: ' ', 18 | FFFT: '▗', 19 | FFTF: '▝', 20 | FFTT: '▐', 21 | FTFF: '▖', 22 | FTFT: '▄', 23 | FTTF: '▞', 24 | FTTT: '▟', 25 | TFFF: '▘', 26 | TFFT: '▚', 27 | TFTF: '▀', 28 | TFTT: '▜', 29 | TTFF: '▌', 30 | TTFT: '▙', 31 | TTTF: '▛', 32 | TTTT: '█', 33 | } as const; 34 | 35 | const quadMapper: Unary>>[], string> = FN.flow( 36 | AR.map( 37 | FN.flow( 38 | flattenPair, 39 | AR.map(foldPx), 40 | join(''), 41 | q => quadToGlyph[q as keyof typeof quadToGlyph] ?? `?`, 42 | pixelColor, 43 | ), 44 | ), 45 | join(''), 46 | ); 47 | 48 | return FN.pipe(bm, chunk4x, AR.map(quadMapper)); 49 | }; 50 | 51 | export const quadRes = quadResWith(FN.identity); 52 | -------------------------------------------------------------------------------- /src/bitmap/role.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { dup, toSnd } from 'fp-ts-std/Tuple'; 3 | import { Unary } from 'util/function'; 4 | import { typedFromEntries, typedKeys } from 'util/object'; 5 | import { Matrix, parse } from './data'; 6 | import { cross } from './data/cross'; 7 | import { elbow } from './data/elbow'; 8 | import { halftone } from './data/halftone'; 9 | import { line } from './data/line'; 10 | import { tee } from './data/tee'; 11 | 12 | const roleToData = { 13 | hLine: line.horizontal, 14 | vLine: line.vertical, 15 | elbow, 16 | hTee: tee.horizontal, 17 | vTee: tee.vertical, 18 | cross, 19 | halftone, 20 | } as const; 21 | 22 | export type BitmapRole = keyof typeof roleToData; 23 | 24 | export const bitmapRoles: BitmapRole[] = typedKeys(roleToData) as BitmapRole[]; 25 | 26 | export const bitmapRole = FN.pipe( 27 | bitmapRoles, 28 | AR.map(dup), 29 | typedFromEntries, 30 | ) as { 31 | [K in BitmapRole]: K; 32 | }; 33 | 34 | const parseRole: Unary = role => 35 | parse(roleToData[role]); 36 | 37 | export const parseRoles: FN.Lazy<[BitmapRole, [string, Matrix][]][]> = () => 38 | FN.pipe(bitmapRoles, FN.pipe(parseRole, toSnd, AR.map)); 39 | -------------------------------------------------------------------------------- /src/bitmap/tests/quadRes.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import { quadRes } from '../quadRes'; 4 | import { registry as reg } from '../registry'; 5 | 6 | suite('quad resolution', () => { 7 | const iut = FN.flow(reg.matrixByChar, quadRes); 8 | suite('unframed', () => { 9 | test('solid', () => 10 | assert.deepEqual(iut(reg.solid), [ 11 | '████', // 12 | '████', 13 | '████', 14 | '████', 15 | ])); 16 | 17 | test('cross', () => 18 | assert.deepEqual(iut('┼'), [ 19 | ' ▐▌ ', // 20 | '▄▟▙▄', 21 | '▀▜▛▀', 22 | ' ▐▌ ', 23 | ])); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/bitmap/tests/registry.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as DA from '../data'; 3 | import { registry as reg } from '../registry'; 4 | 5 | suite('matrix registry', () => { 6 | test('rolesByChar', () => assert.deepEqual(reg.rolesByChar('╯'), ['elbow'])); 7 | 8 | test('chars', () => assert.isAbove(reg.chars.length, 100)); 9 | 10 | test('getMatrix', () => 11 | assert.deepEqual(reg.matrixByChar('█'), DA.fullMatrix)); 12 | 13 | test('char with >1 roles', () => 14 | assert.deepEqual(reg.rolesByChar(' '), ['hLine', 'vLine', 'halftone'])); 15 | }); 16 | -------------------------------------------------------------------------------- /src/block.ts: -------------------------------------------------------------------------------- 1 | export * from './block/block'; 2 | -------------------------------------------------------------------------------- /src/block/block.ts: -------------------------------------------------------------------------------- 1 | import * as build from './build'; 2 | import * as instances from './instances'; 3 | import * as lens from './lens'; 4 | import * as paint from './paint'; 5 | import * as rect from './rect'; 6 | import * as types from './types'; 7 | 8 | export type { Block, BlockArgs } from './types'; 9 | export type { Lenses, LensKey } from './lens'; 10 | 11 | const fns = { 12 | ...types, 13 | ...build, 14 | ...rect, 15 | ...lens, 16 | ...paint, 17 | ...instances, 18 | ...rect.withRect, 19 | } as const; 20 | 21 | export type block = typeof build.build & typeof fns; 22 | 23 | export const block = build.build as block; 24 | 25 | Object.assign(block, fns); 26 | -------------------------------------------------------------------------------- /src/block/instances.ts: -------------------------------------------------------------------------------- 1 | import { string as STR, eq as EQ, function as FN, show as SH } from 'fp-ts'; 2 | import { unwords } from 'fp-ts-std/String'; 3 | import * as GR from 'src/grid'; 4 | import { showRect } from './rect'; 5 | import { Block } from './types'; 6 | import { rect } from 'src/geometry'; 7 | import { align } from 'src/align'; 8 | import * as BD from 'src/backdrop'; 9 | 10 | export const Show: SH.Show = { 11 | show: b => FN.pipe([showRect(b), GR.show.show(b.grid)], unwords), 12 | }; 13 | 14 | export const show = Show.show; 15 | 16 | export const eq: EQ.Eq = EQ.struct({ 17 | grid: GR.eq, 18 | rect: rect.eq, 19 | align: align.eq, 20 | blend: STR.Eq, 21 | backdrop: BD.eq, 22 | }); 23 | -------------------------------------------------------------------------------- /src/block/paint.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { withSnd } from 'fp-ts-std/Tuple'; 3 | import * as BD from 'src/backdrop'; 4 | import * as GR from 'src/grid'; 5 | import { Grid } from 'src/grid'; 6 | import { BinaryC, Unary } from 'util/function'; 7 | import * as BLE from './lens'; 8 | import { withRect as RCT } from './rect'; 9 | import { Block } from './types'; 10 | 11 | /** Render grid and align results inside a rectangle */ 12 | export const paint: Unary = b => { 13 | const [align, size, backdrop] = [ 14 | BLE.align.get(b), 15 | RCT.size.get(b), 16 | BLE.backdrop.get(b), 17 | ]; 18 | 19 | const grid = GR.alignGrid(align, size)(b.grid); 20 | 21 | if (BD.isEmpty(backdrop)) return grid; 22 | 23 | return FN.pipe( 24 | size, 25 | BD.paint(backdrop), 26 | withSnd(grid), 27 | FN.pipe(b, BLE.blend.get, GR.stackAlign([align, size])), 28 | ); 29 | }; 30 | 31 | export const asStringsWith: BinaryC = s => 32 | FN.flow(paint, GR.paintWith(s)); 33 | 34 | export const asStrings: Unary = asStringsWith(' '); 35 | -------------------------------------------------------------------------------- /src/block/tests/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { align } from 'src/align'; 3 | import { Block, block } from 'src/block'; 4 | import { size } from 'src/geometry'; 5 | import { Pair } from 'util/tuple'; 6 | import * as GR from 'src/grid'; 7 | import { assert, suite, test } from 'vitest'; 8 | 9 | suite('block basic', () => { 10 | const tiny = block.fromRow('X'), 11 | small = FN.pipe('foo', block.fromRow, block.align.set(align.topCenter)), 12 | big = FN.pipe( 13 | ['bar', 'quux'], 14 | block.fromRows, 15 | block.align.set(align.topCenter), 16 | ); 17 | 18 | suite('block size', () => { 19 | const check = (name: string, iut: Block, expect: Pair) => 20 | test(name, () => 21 | assert.deepEqual(block.size.get(iut), size.tupled(expect)), 22 | ); 23 | 24 | check('tiny', tiny, [1, 1]); 25 | check('small', small, [3, 1]); 26 | check('big', big, [4, 2]); 27 | }); 28 | 29 | suite('resetSize', () => { 30 | const newGrid = GR.parseRow('XY'), 31 | widerTiny = FN.pipe(tiny, block.grid.set(newGrid)); 32 | 33 | test('set rows but no reset size', () => 34 | assert.equal(block.width.get(widerTiny), 1)); 35 | 36 | test('set rows with reset size', () => 37 | assert.equal(FN.pipe(widerTiny, block.resetSize, block.width.get), 2)); 38 | }); 39 | 40 | test('rect lens', () => assert.equal(block.zOrder.get(tiny), 0)); 41 | }); 42 | -------------------------------------------------------------------------------- /src/block/tests/instances.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import { block } from 'src/block'; 3 | import { defaultBlendMode } from 'src/color/blend'; 4 | 5 | suite('block instances', () => { 6 | suite('show', () => { 7 | test('empty', () => 8 | assert.deepEqual( 9 | block.show(block.empty), 10 | `▲0:◀0 ↔0:↕0 ⭹ ${defaultBlendMode} 0ˣ0 0% non-empty`, 11 | )); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/block/tests/paint.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { align as AL, Align } from 'src/align'; 3 | import { assert, suite, test } from 'vitest'; 4 | import * as BU from '../build'; 5 | import * as PA from '../paint'; 6 | import * as TY from '../types'; 7 | 8 | suite('block paint', () => { 9 | const checkPaint = 10 | (height = 3) => 11 | (expect: string[], align: Align) => 12 | test(AL.show(align), () => 13 | assert.deepEqual( 14 | FN.pipe( 15 | { rows: ['a', 'ab', 'abcd'], align, height }, 16 | BU.build, 17 | PA.asStringsWith('.'), 18 | ), 19 | expect, 20 | ), 21 | ); 22 | 23 | suite('hAlign', () => { 24 | const check = checkPaint(); 25 | check(['a...', 'ab..', 'abcd'], TY.defaultAlign); 26 | check(['...a', '..ab', 'abcd'], AL.bottomRight); 27 | check(['.a..', '.ab.', 'abcd'], AL.topCenter); 28 | }); 29 | 30 | suite('vAlign', () => { 31 | const check = checkPaint(5); 32 | check(['....', '....', 'a...', 'ab..', 'abcd'], TY.defaultAlign); 33 | check(['....', 'a...', 'ab..', 'abcd', '....'], AL.middleLeft); 34 | check(['.a..', '.ab.', 'abcd', '....', '....'], AL.topCenter); 35 | check(['....', '.a..', '.ab.', 'abcd', '....'], AL.middleCenter); 36 | }); 37 | 38 | suite('shrink', () => { 39 | const check = checkPaint(1); 40 | 41 | check(['abcd'], TY.defaultAlign); 42 | check(['a...'], AL.topLeft); 43 | check(['.ab.'], AL.middleCenter); 44 | }); 45 | 46 | suite('double width characters', () => { 47 | const iut = BU.fromRow('🙂'); 48 | 49 | test('paint', () => 50 | assert.deepEqual(FN.pipe(iut, PA.asStringsWith('.')), ['🙂'])); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/border.ts: -------------------------------------------------------------------------------- 1 | export * from './border/border'; 2 | -------------------------------------------------------------------------------- /src/border/border.ts: -------------------------------------------------------------------------------- 1 | import { apply, named } from './apply'; 2 | import * as build from './build'; 3 | import * as backdrop from './backdrop'; 4 | import * as edge from './edge'; 5 | import * as mask from './mask'; 6 | import * as ops from './ops'; 7 | import * as part from './part'; 8 | import { sets } from './sets'; 9 | import * as types from './types'; 10 | import * as variants from './variants'; 11 | 12 | export type { 13 | BackdropParts, 14 | Border, 15 | BorderEdge, 16 | BorderLines, 17 | BorderName, 18 | CellParts, 19 | CharParts, 20 | DashBorderName, 21 | EdgeParts, 22 | NoDashBorderName, 23 | } from './types'; 24 | export type { sets } from './sets'; 25 | export type { Mask } from './mask'; 26 | 27 | const fns = { 28 | apply, 29 | names: types.borderNames, 30 | ...types, 31 | ...backdrop, 32 | ...build, 33 | ...edge, 34 | ...mask, 35 | ...ops, 36 | ...part, 37 | ...variants, 38 | ...named, 39 | sets, 40 | }; 41 | 42 | export type border = typeof apply & typeof fns; 43 | 44 | export const border = apply as border; 45 | 46 | Object.assign(border, fns); 47 | -------------------------------------------------------------------------------- /src/border/build.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, option as OP } from 'fp-ts'; 2 | import { Backdrop } from 'src/backdrop'; 3 | import * as BD from 'src/backdrop'; 4 | import { box, MaybeBox } from 'src/box'; 5 | import { BorderDir, borderDir } from 'src/geometry'; 6 | import { Unary } from 'util/function'; 7 | import { mapValuesOf } from 'util/object'; 8 | import { BackdropParts, Border, CharParts, CellParts } from './types'; 9 | import * as cell from 'src/cell'; 10 | import { Cell } from 'src/cell'; 11 | 12 | const backdropBox: Unary = FN.flow( 13 | box.fromBackdrop, 14 | OP.some, 15 | ); 16 | 17 | const mapBordered = () => mapValuesOf(); 18 | 19 | /** Create a border from the given mapping of `BorderDir ⇒ Backdrop */ 20 | export const fromBackdrops: Unary = 21 | mapBordered()(backdropBox); 22 | 23 | /** Create a border that repeats the given cell */ 24 | export const fromCells: Unary = mapBordered()( 25 | FN.flow(BD.repeatCell, backdropBox), 26 | ); 27 | 28 | /** Create a border that repeats the char given per border direction */ 29 | export const fromNarrowChars: Unary = FN.flow( 30 | mapValuesOf()(cell.plainChar), 31 | fromCells, 32 | ); 33 | 34 | export const fromChar: Unary = FN.flow( 35 | box.fromRow, 36 | OP.some, 37 | borderDir.singleton, 38 | ); 39 | 40 | export const empty: Border = borderDir.singleton(OP.none); 41 | -------------------------------------------------------------------------------- /src/border/sets.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, readonlyArray as RA } from 'fp-ts'; 2 | import { toSnd } from 'fp-ts-std/Tuple'; 3 | import { bitmap } from 'src/bitmap'; 4 | import { Unary } from 'util/function'; 5 | import { typedFromEntries } from 'util/object'; 6 | import { fromNarrowChars } from './build'; 7 | import { borderLines } from './lines'; 8 | import { 9 | Border, 10 | BorderName, 11 | borderNames, 12 | CharParts, 13 | DashBorderName, 14 | isDashBorderName, 15 | NoDashBorderName, 16 | } from './types'; 17 | 18 | const noDashBorderName = (n: DashBorderName): NoDashBorderName => 19 | n.startsWith('round') ? 'round' : n.startsWith('thick') ? 'thick' : 'line'; 20 | 21 | const byName: Unary = name => ({ 22 | ...bitmap.elbowByGroup( 23 | isDashBorderName(name) ? noDashBorderName(name) : name, 24 | ), 25 | ...borderLines(name), 26 | }); 27 | 28 | const makeBorder: Unary = FN.pipe( 29 | FN.flow(byName, fromNarrowChars), 30 | toSnd, 31 | ); 32 | 33 | export const sets: Record = FN.pipe( 34 | borderNames, 35 | RA.map(makeBorder), 36 | typedFromEntries, 37 | ); 38 | -------------------------------------------------------------------------------- /src/border/tests/apply.test.ts: -------------------------------------------------------------------------------- 1 | import { border } from 'src/border'; 2 | import { suite } from 'vitest'; 3 | import { testBorder, testBorderWith } from './helpers'; 4 | 5 | suite('border apply', () => { 6 | testBorder('basic', border.sets.line, [ 7 | '┌─┐', // 8 | '│X│', 9 | '└─┘', 10 | ]); 11 | 12 | testBorderWith('🙂')('wide characters', border.sets.line, [ 13 | '┌──┐', // 14 | '│🙂│', 15 | '└──┘', 16 | ]); 17 | }); 18 | -------------------------------------------------------------------------------- /src/border/tests/edge.test.ts: -------------------------------------------------------------------------------- 1 | import { border } from 'src/border'; 2 | import { suite } from 'vitest'; 3 | import { testBorder } from './helpers'; 4 | 5 | suite('border edge', () => { 6 | const iut = border.sets.line; 7 | 8 | testBorder('noHEdges', border.mask.noHEdges(iut), ['│X│']); 9 | 10 | testBorder('noVEdges', border.mask.noVEdges(iut), [ 11 | '─', // 12 | 'X', 13 | '─', 14 | ]); 15 | 16 | testBorder('copyHEdges', border.copyHEdges([iut, border.sets.thick]), [ 17 | '┌━┐', // 18 | '│X│', 19 | '└━┘', 20 | ]); 21 | 22 | testBorder('copyVEdges', border.copyHEdges([border.sets.double, iut]), [ 23 | '╔─╗', // 24 | '║X║', 25 | '╚─╝', 26 | ]); 27 | }); 28 | -------------------------------------------------------------------------------- /src/border/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { Border, border } from 'src/border'; 3 | import { box } from 'src/box'; 4 | import { assert, test } from 'vitest'; 5 | 6 | export const testBorderWith = 7 | (content: string) => (name: string, actual: Border, expect: string[]) => 8 | test(name, () => 9 | assert.deepEqual( 10 | FN.pipe(content, box.fromRow, border(actual), box.asStringsWith('.')), 11 | expect, 12 | ), 13 | ); 14 | 15 | export const testBorder = testBorderWith('X'); 16 | -------------------------------------------------------------------------------- /src/border/tests/lines.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import { borderLines } from '../lines'; 3 | 4 | suite('border lines', () => { 5 | test('halfSolid', () => 6 | assert.deepStrictEqual(borderLines('halfSolid'), { 7 | top: '▀', 8 | left: '▌', 9 | bottom: '▄', 10 | right: '▐', 11 | })); 12 | test('hHalfSolid', () => 13 | assert.deepStrictEqual(borderLines('hHalfSolid'), { 14 | top: '▄', 15 | left: '▌', 16 | bottom: '▀', 17 | right: '▐', 18 | })); 19 | }); 20 | -------------------------------------------------------------------------------- /src/border/tests/part.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { border } from 'src/border'; 3 | import { box } from 'src/box'; 4 | import { suite } from 'vitest'; 5 | import { testBorder } from './helpers'; 6 | 7 | suite('border part', () => { 8 | const iut = border.sets.line; 9 | 10 | testBorder('noHlines', border.mask.noHLines(iut), [ 11 | '┌.┐', // 12 | '│X│', 13 | '└.┘', 14 | ]); 15 | 16 | testBorder('noVlines', border.mask.noVLines(iut), [ 17 | '┌─┐', // 18 | '.X.', 19 | '└─┘', 20 | ]); 21 | 22 | testBorder( 23 | 'setHParts', 24 | FN.pipe(iut, FN.pipe('A', box.fromRow, border.setHParts)), 25 | [ 26 | '┌A┐', // 27 | '│X│', 28 | '└A┘', 29 | ], 30 | ); 31 | 32 | testBorder('noCorners', border.mask.noCorners(iut), [ 33 | '.─.', // 34 | '│X│', 35 | '.─.', 36 | ]); 37 | 38 | testBorder('noLines', border.mask.noLines(iut), [ 39 | '┌.┐', // 40 | '.X.', 41 | '└.┘', 42 | ]); 43 | }); 44 | -------------------------------------------------------------------------------- /src/border/tests/variants.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { bitmap } from 'src/bitmap'; 3 | import { border } from 'src/border'; 4 | import { box } from 'src/box'; 5 | import { assert, suite, test } from 'vitest'; 6 | import { testBorder } from './helpers'; 7 | 8 | suite('border variants', () => { 9 | testBorder('hLines', border.hLines, [ 10 | '─', // 11 | 'X', 12 | '─', 13 | ]); 14 | 15 | testBorder('vLines', border.vLines, ['│X│']); 16 | 17 | test('hRule', () => 18 | assert.deepEqual(FN.pipe(border.hRule(3), box.asStrings), [ 19 | AR.replicate(3, bitmap.line.dash.wide.horizontal).join(''), 20 | ])); 21 | }); 22 | -------------------------------------------------------------------------------- /src/box.ts: -------------------------------------------------------------------------------- 1 | export * from './box/box'; 2 | -------------------------------------------------------------------------------- /src/box/align.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, readonlyArray as RA } from 'fp-ts'; 2 | import * as AL from 'src/align'; 3 | import { Endo } from 'util/function'; 4 | import * as BLE from './block'; 5 | import { exportRect as RCT } from './rect'; 6 | import { Box, BoxGet } from './types'; 7 | import { Rect, rect } from 'src/geometry'; 8 | import * as NO from './nodes'; 9 | 10 | export const [align, hAlign, vAlign] = [BLE.align, BLE.hAlign, BLE.vAlign]; 11 | 12 | /** 13 | * Match box vertical alignment. Runs one of the given functions, depending on 14 | * alignment. 15 | */ 16 | export const matchVAlign = (top: R, middle: R, bottom: R): BoxGet => 17 | FN.flow(vAlign.get, AL.matchVAlign(top, middle, bottom)); 18 | 19 | export const [alignL, alignC, alignR] = FN.pipe(AL.hAlign, RA.map(hAlign.set)), 20 | [alignT, alignM, alignB] = FN.pipe(AL.vAlign, RA.map(vAlign.set)); 21 | 22 | export const center = FN.flow(alignC, alignM); 23 | 24 | /** 25 | * Set box size to bounds of its own content and it child content 26 | * 27 | * This makes sure it is large enough to show both. 28 | */ 29 | export const sizeToContent: Endo = b => { 30 | const childBounds: Rect = FN.pipe(b, NO.measureNodes, rect.atOrigin), 31 | ownBounds = FN.pipe(b, BLE.measureGrid, rect.atOrigin), 32 | size = rect.add(childBounds, ownBounds).size; 33 | 34 | return FN.pipe(b, RCT.size.set(size)); 35 | }; 36 | -------------------------------------------------------------------------------- /src/box/box.ts: -------------------------------------------------------------------------------- 1 | import * as nodes from './nodes'; 2 | import * as align from './align'; 3 | import * as block from './block'; 4 | import { buildBox } from './build'; 5 | import * as build from './build'; 6 | import * as paint from './paint'; 7 | import * as place from './place'; 8 | import { exportRect } from './rect'; 9 | import * as move from './move'; 10 | import * as cat from './cat'; 11 | import * as debug from './report'; 12 | import * as margins from './margins'; 13 | 14 | export * from './types'; 15 | 16 | const fns = { 17 | ...build, 18 | ...block, 19 | ...exportRect, 20 | ...nodes, 21 | ...align, 22 | ...move, 23 | ...place, 24 | ...paint, 25 | ...cat, 26 | ...margins, 27 | debug, 28 | } as const; 29 | 30 | export type box = typeof buildBox & typeof fns; 31 | 32 | export const box = buildBox as box; 33 | 34 | Object.assign(box, fns); 35 | -------------------------------------------------------------------------------- /src/box/margins.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { Orientation, Directed } from 'src/geometry'; 3 | import { Pair } from 'util/tuple'; 4 | import { buildBox } from './build'; 5 | import { above, below, leftOf, rightOf } from './place'; 6 | import { BoxSet } from './types'; 7 | 8 | export const [marginLeft, marginRight]: Pair> = [ 9 | width => FN.pipe({ width }, buildBox, rightOf), 10 | width => FN.pipe({ width }, buildBox, leftOf), 11 | ], 12 | [marginTop, marginBottom]: Pair> = [ 13 | height => FN.pipe({ height }, buildBox, above), 14 | height => FN.pipe({ height }, buildBox, below), 15 | ]; 16 | 17 | export const margins: BoxSet> = ({ 18 | top, 19 | right, 20 | bottom, 21 | left, 22 | }) => 23 | FN.flow( 24 | marginTop(top), 25 | marginRight(right), 26 | marginBottom(bottom), 27 | marginLeft(left), 28 | ); 29 | 30 | export const axisMargins: BoxSet>> = ({ 31 | horizontal = 0, 32 | vertical = 0, 33 | }) => 34 | margins({ 35 | top: vertical, 36 | right: horizontal, 37 | bottom: vertical, 38 | left: horizontal, 39 | }); 40 | 41 | export const margin: BoxSet = m => 42 | margins({ top: m, right: m, bottom: m, left: m }); 43 | 44 | export const [hMargins, vMargins]: Pair> = [ 45 | n => axisMargins({ horizontal: n }), 46 | n => axisMargins({ vertical: n }), 47 | ]; 48 | -------------------------------------------------------------------------------- /src/box/place.ts: -------------------------------------------------------------------------------- 1 | export * from './place/placements'; 2 | export * from './place/ops'; 3 | export * from './place/names'; 4 | -------------------------------------------------------------------------------- /src/box/place/ops.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { curry2T, uncurry2 } from 'fp-ts-std/Function'; 3 | import * as AL from 'src/align'; 4 | import { Binary, BinaryC } from 'util/function'; 5 | import * as MV from '../move'; 6 | import { branch } from '../paint'; 7 | import { OpC } from '../types'; 8 | 9 | /** `moveAlignGap` that creates a curried binary operation */ 10 | export const placeBesides: BinaryC = 11 | alignPair => gap => anchor => placed => 12 | FN.pipe([anchor, placed], MV.moveAlignGap(gap)(alignPair)); 13 | 14 | /** Uncurried version of `placeBesides` */ 15 | export const placeBesidesU: Binary = FN.pipe( 16 | placeBesides, 17 | uncurry2, 18 | FN.untupled, 19 | ); 20 | 21 | /** 22 | * Create a curried binary operator that places boxes according to the given 23 | * `AlignPair` separated by the given gap. 24 | * 25 | * Like `placeBesides` except it creates a new parent and returns it instead of 26 | * the placed box. This new box will contain both the anchor and the placed box. 27 | * 28 | * For example to create an operator that layouts boxes horizontally, top 29 | * aligned, and with no gap: 30 | * 31 | * ```ts 32 | * const myOp = place(['right','top'])(0); 33 | * ``` 34 | * 35 | * When `myOp` is called with an anchor box, and then with the box to be placed, 36 | * it will return a new box with the anchor and the placed box as its children, 37 | * where the placed box is zero columns to the right of the anchor, top aligned. 38 | */ 39 | export const place: typeof placeBesides = alignPair => gap => anchor => 40 | FN.flow( 41 | FN.pipe(anchor, placeBesidesU(alignPair, gap)), 42 | FN.pipe(anchor, curry2T(branch)), 43 | ); 44 | -------------------------------------------------------------------------------- /src/box/rect.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import * as LE from 'monocle-ts/lib/Lens'; 3 | import * as BL from 'src/block'; 4 | import * as GE from 'src/geometry'; 5 | import { Unary } from 'util/function'; 6 | import { ModLens, modLens } from 'util/lens'; 7 | import * as BLE from './block'; 8 | import { Box } from './types'; 9 | 10 | const block = BL.block; 11 | const boxBlock = BLE.block; 12 | 13 | const rect: ModLens = FN.pipe( 14 | boxBlock, 15 | LE.compose(block.rect), 16 | modLens, 17 | ); 18 | 19 | const maxWidth: Unary = FN.flow( 20 | AR.map(boxBlock.get), 21 | block.maxWidth, 22 | ); 23 | 24 | export const exportRect = { 25 | rect, 26 | 27 | ...block.delegateRect(rect), 28 | 29 | incZOrder: boxBlock.mod(block.incZOrder), 30 | decZOrder: boxBlock.mod(block.decZOrder), 31 | unsetZOrder: boxBlock.mod(block.unsetZOrder), 32 | 33 | corners: FN.flow(boxBlock.get, block.corners), 34 | hasSize: FN.flow(boxBlock.get, block.hasSize), 35 | area: FN.flow(boxBlock.get, block.area), 36 | 37 | translateToPositive: block.translateToPositiveFor(rect), 38 | minTopLeft: block.minTopLeft, 39 | incSize: boxBlock.mod(block.incSize), 40 | decSize: boxBlock.mod(block.incSize), 41 | 42 | maxWidth, 43 | } as const; 44 | -------------------------------------------------------------------------------- /src/box/report.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, monoid as MO, number as NU } from 'fp-ts'; 2 | import { Block, block } from 'src/block'; 3 | import { Unary } from 'util/function'; 4 | import * as TR from 'util/tree'; 5 | import { Box } from './types'; 6 | 7 | /** 8 | * ```txt 9 | * grid non-empty 10 | * top width align width cells % 11 | * ↓ ↓ ↓ ↓ ↓ 12 | * ▲1:◀0 ↔2:↕1 ⭹ screen 2ˣ1 100% 13 | * ↑ ↑ ↑ ↑ 14 | * left height blend grid 15 | * mode height 16 | * 17 | * ``` 18 | */ 19 | 20 | const blockSummary: Unary = block.show; 21 | 22 | const toStringTree: Unary> = TR.mapTree(blockSummary); 23 | 24 | export const drawTree: Unary = FN.flow(toStringTree, TR.draw); 25 | 26 | interface Report { 27 | readonly nodeCount: number; 28 | readonly show: string[]; 29 | } 30 | 31 | const reportMonoid = MO.struct({ 32 | nodeCount: NU.MonoidSum, 33 | show: AR.getMonoid(), 34 | }); 35 | 36 | export const reportAlgebra: TR.TreeAlgebra = ({ 37 | value, 38 | nodes, 39 | }) => { 40 | const { nodeCount, show } = FN.pipe(nodes, MO.concatAll(reportMonoid)); 41 | return { 42 | nodeCount: nodeCount + 1, 43 | show: [...show, block.show(value) + ` nodes=${nodeCount}`], 44 | }; 45 | }; 46 | 47 | export const printTree = (box: Box) => console.log(drawTree(box)); 48 | -------------------------------------------------------------------------------- /src/box/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Box } from '../types'; 2 | import * as GR from 'src/grid'; 3 | import { Unary } from 'util/function'; 4 | import { assert, test } from 'vitest'; 5 | import * as PA from '../paint'; 6 | 7 | export const paint: Unary = PA.asStringsWith('.'); 8 | 9 | export const showGrid = GR.paintWith('.'); 10 | 11 | export const testPaint = (name: string, iut: Box, expect: string[]) => { 12 | const actual = paint(iut); 13 | test(name, () => assert.deepEqual(actual, expect)); 14 | }; 15 | 16 | export const testPaints = ( 17 | ...tests: [name: string, init: Box, expect: string[]][] 18 | ) => { 19 | for (const [name, init, expect] of tests) testPaint(name, init, expect); 20 | }; 21 | -------------------------------------------------------------------------------- /src/box/tests/instances.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { box } from 'src/box'; 3 | import * as color from 'src/color'; 4 | import { test, assert, suite } from 'vitest'; 5 | 6 | const blend = color.defaultBlendMode; 7 | 8 | suite('box instances', () => { 9 | suite('show', () => { 10 | test('leaf', () => 11 | assert.equal( 12 | box.show(box({ row: 'foo' })), 13 | `leaf(▲0:◀0 ↔3:↕1 ⭹ ${blend} 3ˣ1 100% non-empty)`, 14 | )); 15 | test('branch', () => 16 | assert.equal( 17 | FN.pipe( 18 | 'foo', 19 | box.fromRow, 20 | FN.pipe('bar', box.fromRow, box.below), 21 | box.show, 22 | ), 23 | `tree(▲0:◀0 ↔3:↕2 ⭹ ${blend} 0ˣ0 0% non-empty)` + 24 | '([' + 25 | `leaf(▲0:◀0 ↔3:↕1 ⭹ ${blend} 3ˣ1 100% non-empty)` + 26 | ', ' + 27 | `leaf(▲1:◀0 ↔3:↕1 ⭹ ${blend} 3ˣ1 100% non-empty)` + 28 | '])', 29 | )); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/box/tests/pos.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, tuple as TU } from 'fp-ts'; 2 | import { box, Box } from 'src/box'; 3 | import { pos } from 'src/geometry'; 4 | import { Pair } from 'util/tuple'; 5 | import { assert, suite, test } from 'vitest'; 6 | 7 | suite('pos', () => { 8 | test('subTop', () => 9 | assert.equal( 10 | FN.pipe('foo', box.fromRow, box.subTop(42), box.top.get), 11 | -42, 12 | )); 13 | 14 | suite('normalize', () => { 15 | const noPos: Pair = [box.fromRow('fst'), box.fromRow('snd')]; 16 | 17 | const setPos = FN.flow(pos, box.pos.set); 18 | 19 | test('one child', () => { 20 | const positioned = FN.pipe(noPos, TU.fst, setPos(-1, -1)); 21 | 22 | const normalized = box.translateToPositive([positioned]); 23 | 24 | assert.deepEqual(FN.pipe(normalized, AR.map(box.pos.get)), [pos(0, 0)]); 25 | }); 26 | 27 | test('negative', () => { 28 | const positioned = FN.pipe(noPos, TU.bimap(setPos(3, -2), setPos(-1, 5))); 29 | 30 | const normalized = box.translateToPositive(positioned); 31 | 32 | assert.deepEqual(FN.pipe(normalized, AR.map(box.pos.get)), [ 33 | pos(0, 7), 34 | pos(4, 0), 35 | ]); 36 | }); 37 | 38 | test('positive', () => { 39 | const positioned = FN.pipe(noPos, TU.bimap(setPos(3, 2), setPos(1, 5))); 40 | const normalized = box.translateToPositive(positioned); 41 | 42 | assert.deepEqual(FN.pipe(normalized, AR.map(box.pos.get)), [ 43 | pos(1, 5), 44 | pos(3, 2), 45 | ]); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/boxes.ts: -------------------------------------------------------------------------------- 1 | export * from './boxes/boxes'; 2 | -------------------------------------------------------------------------------- /src/boxes/basic.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { border } from 'src/border'; 3 | import { box } from 'src/box'; 4 | 5 | export const buildLine: typeof box.buildBox = FN.flow(box, border.line); 6 | 7 | const lineFns = { 8 | of: FN.flow(box.of, border.line), 9 | fromRow: FN.flow(box.fromRow, border.line), 10 | fromWords: FN.flow(box.fromWords, border.line), 11 | fromRows: FN.flow(box.fromRows, border.line), 12 | centered: FN.flow(box.centered, border.line), 13 | } as const; 14 | 15 | export type line = typeof buildLine & typeof lineFns; 16 | export const line = buildLine as line; 17 | 18 | Object.assign(line, lineFns); 19 | -------------------------------------------------------------------------------- /src/boxes/boxes.ts: -------------------------------------------------------------------------------- 1 | import * as flow from './flow'; 2 | import * as win from './win'; 3 | import * as labeled from './labeled'; 4 | import * as basic from './basic'; 5 | 6 | export type { FlowConfig, MinFlowConfig } from './flow'; 7 | export type { WinFlowConfig } from './win'; 8 | 9 | export const boxes = { 10 | ...flow, 11 | ...labeled, 12 | ...basic, 13 | win, 14 | line: basic.line, 15 | } as const; 16 | -------------------------------------------------------------------------------- /src/boxes/tests/flow.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, number as NU } from 'fp-ts'; 2 | import { flip } from 'fp-ts-std/Function'; 3 | import { box, Cat, Box } from 'src/box'; 4 | import { boxes } from 'src/boxes'; 5 | import { mapRange } from 'util/array'; 6 | import { Unary } from 'util/function'; 7 | import { assert, suite, test } from 'vitest'; 8 | 9 | const runTest = 10 | (boxWidth: number, name: string, f: Unary) => 11 | (available: number, boxCount: number, expect: string[]) => { 12 | test(`${name} wanted:${boxCount}, available:${available}`, () => { 13 | const actual = FN.pipe( 14 | FN.flow(NU.Show.show, box.fromRow), 15 | FN.pipe([0, boxCount - 1], flip(mapRange)), 16 | FN.pipe(boxWidth, box.width.set, AR.map), 17 | f(available), 18 | box.asStringsWith('.'), 19 | ); 20 | assert.deepEqual(actual, expect); 21 | }); 22 | }; 23 | 24 | const testFlow = runTest(1, 'flow', boxes.flow); 25 | const testHGap = runTest(1, 'hGap', boxes.flow.hGap(2)); 26 | 27 | suite('flow placement combinator', () => { 28 | testFlow(5, 3, ['012']); 29 | testFlow(2, 3, ['01', '2.']); 30 | testFlow(2, 4, ['01', '23']); 31 | testFlow(2, 5, ['01', '23', '4.']); 32 | testFlow(3, 4, ['012', '3..']); 33 | testFlow(3, 7, ['012', '345', '6..']); 34 | 35 | testHGap(4, 2, ['0..1']); 36 | testHGap(4, 5, ['0..1', '2..3', '4...']); 37 | }); 38 | -------------------------------------------------------------------------------- /src/cell.ts: -------------------------------------------------------------------------------- 1 | export * from './cell/cell'; 2 | -------------------------------------------------------------------------------- /src/cell/build.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { Color } from 'src/color'; 3 | import stringWidth from 'string-width'; 4 | import { BinaryC, Unary } from 'util/function'; 5 | import { Pair, Tuple3 } from 'util/tuple'; 6 | import * as ST from '../style'; 7 | import { Style } from '../style'; 8 | import { Cell } from './types'; 9 | 10 | export const [none, cont]: Pair = [ 11 | [ST.empty, '', 'none'], 12 | [ST.empty, '', 'cont'], 13 | ]; 14 | 15 | export const narrow: Unary<[Style, string], Cell> = ([style, char]) => [ 16 | style, 17 | char, 18 | 'char', 19 | ]; 20 | 21 | export const wide: Unary<[Style, string], Cell[]> = ([style, char]) => [ 22 | [style, char, 'wide'], 23 | cont, 24 | ]; 25 | 26 | export const build: BinaryC = style => char => 27 | char === '' || (char === ' ' && ST.isEmpty(style)) 28 | ? [none] 29 | : stringWidth(char) === 1 30 | ? [narrow([style, char])] 31 | : wide([style, char]); 32 | 33 | export const [spaceBg, solidFg]: Pair> = [ 34 | c => narrow([ST.fromBg(c), ' ']), 35 | c => narrow([ST.fromFg(c), '█']), 36 | ]; 37 | 38 | export const [fgChar, bgChar]: Pair> = [ 39 | FN.flow(ST.fromFg, build), 40 | FN.flow(ST.fromBg, build), 41 | ]; 42 | 43 | export const colored: BinaryC, string, Cell[]> = FN.flow( 44 | ST.colored, 45 | build, 46 | ); 47 | 48 | export const [bold, italic, underline]: Tuple3> = [ 49 | FN.pipe(ST.empty, ST.bold, build), 50 | FN.pipe(ST.empty, ST.italic, build), 51 | FN.pipe(ST.empty, ST.underline, build), 52 | ]; 53 | 54 | export const [plainChar, plainWide]: [ 55 | Unary, 56 | Unary, 57 | ] = [s => narrow([ST.empty, s]), s => wide([ST.empty, s])]; 58 | -------------------------------------------------------------------------------- /src/cell/cell.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './stack'; 3 | export * from './instances'; 4 | export * from './lens'; 5 | export * from './ops'; 6 | export * from './paint'; 7 | export * from './parse'; 8 | export * from './packed'; 9 | export * from './build'; 10 | export * from './rune'; 11 | export * from './copy'; 12 | 13 | import { fgLens, bgLens } from './lens'; 14 | import { fgOps, bgOps } from './ops'; 15 | 16 | export type { PackedType, CellType } from './type'; 17 | export type { PrevStyle } from './paint'; 18 | 19 | export const fg = { ...fgLens, ...fgOps }, 20 | bg = { ...bgLens, ...bgOps }; 21 | -------------------------------------------------------------------------------- /src/cell/instances.ts: -------------------------------------------------------------------------------- 1 | import { 2 | array as AR, 3 | eq as EQ, 4 | monoid as MO, 5 | show as SH, 6 | string as STR, 7 | } from 'fp-ts'; 8 | import { BlendMode } from 'src/color'; 9 | import { Unary } from 'util/function'; 10 | import * as ST from 'src/style'; 11 | import * as BU from './build'; 12 | import * as PA from './packed'; 13 | import { stackPacked } from './stack'; 14 | import * as TY from './types'; 15 | import { Cell } from './types'; 16 | 17 | export const eq: EQ.Eq = EQ.tuple(ST.eq, STR.Eq, STR.Eq); 18 | 19 | export const show: SH.Show = { 20 | show: TY.matchCell( 21 | 'none', 22 | ([style, char]) => `char: “${char}” ${ST.show.show(style)}`, 23 | ([style, char]) => `wide: “${char}” ${ST.show.show(style)}`, 24 | 'cont', 25 | ), 26 | }; 27 | 28 | export const getMonoid: Unary> = mode => ({ 29 | empty: [BU.none], 30 | concat: (lower, upper) => 31 | AR.map(PA.unpackCell)( 32 | stackPacked(mode)([ 33 | AR.map(PA.packCell)(lower), 34 | AR.map(PA.packCell)(upper), 35 | ]), 36 | ), 37 | }); 38 | -------------------------------------------------------------------------------- /src/cell/lens.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import * as LE from 'monocle-ts/lib/Lens'; 3 | import * as color from 'src/color'; 4 | import { Color, ComposedColorLenses } from 'src/color'; 5 | import { ModLens, modLens } from 'util/lens'; 6 | import { Pair } from 'util/tuple'; 7 | import * as ST from 'src/style'; 8 | import { Deco, DecoList, Style } from 'src/style'; 9 | import { CellType } from './type'; 10 | import { 11 | Cell, 12 | getBg, 13 | getCellType, 14 | getDeco, 15 | getFg, 16 | getRune, 17 | getStyle, 18 | setBg, 19 | setCellType, 20 | setDeco, 21 | setFg, 22 | setRune, 23 | setStyle, 24 | } from './types'; 25 | 26 | export const style: ModLens = modLens({ 27 | get: getStyle, 28 | set: setStyle, 29 | }), 30 | rune: ModLens = modLens({ get: getRune, set: setRune }), 31 | cellType: ModLens = modLens({ 32 | get: getCellType, 33 | set: setCellType, 34 | }), 35 | [fgColor, bgColor]: Pair> = [ 36 | modLens({ get: getFg, set: setFg }), 37 | modLens({ get: getBg, set: setBg }), 38 | ], 39 | deco: ModLens = modLens({ get: getDeco, set: setDeco }), 40 | decoList: ModLens = FN.pipe( 41 | style, 42 | LE.composeLens(ST.decoList), 43 | modLens, 44 | ); 45 | 46 | export type LayerLens = ComposedColorLenses & 47 | Record<'color', ModLens>; 48 | 49 | export const [fgLens, bgLens]: Pair = [ 50 | { ...color.composeColorLens(fgColor), color: fgColor }, 51 | { ...color.composeColorLens(bgColor), color: bgColor }, 52 | ]; 53 | 54 | export const add = ST.deco.decoRecord(FN.flow(ST.deco.add, deco.mod)); 55 | -------------------------------------------------------------------------------- /src/cell/ops.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import * as color from 'src/color'; 3 | import { Endo } from 'util/function'; 4 | import { Tuple3 } from 'util/tuple'; 5 | import * as ST from 'src/style'; 6 | import { bgColor, deco, fgColor, style } from './lens'; 7 | import { Cell } from './types'; 8 | 9 | export const [clearFg, clearBg, clearDeco]: Tuple3> = [ 10 | fgColor.set(0), 11 | bgColor.set(0), 12 | deco.set(0), 13 | ], 14 | clearColor: Endo = FN.flow(clearFg, clearBg), 15 | flip: Endo = style.mod(ST.flip); 16 | 17 | export const [fgOps, bgOps] = [ 18 | color.delegateOps(fgColor), 19 | color.delegateOps(bgColor), 20 | ]; 21 | -------------------------------------------------------------------------------- /src/cell/parse.ts: -------------------------------------------------------------------------------- 1 | import { AnserJsonEntry } from 'anser'; 2 | import { Unary } from 'util/function'; 3 | import stringWidth from 'string-width'; 4 | import * as ST from '../style'; 5 | import * as PA from './packed'; 6 | import { PackedCell } from './packed'; 7 | import { encode } from './rune'; 8 | 9 | export type ParsedChunk = [chunkCellCount: number, chunkCells: PackedCell[]]; 10 | 11 | const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); 12 | 13 | export const parseCells: Unary = entry => { 14 | const style = ST.fromParsed(entry); 15 | 16 | let chunkWidth = 0; 17 | 18 | const res = Array.from(segmenter.segment(entry.content), s => { 19 | const char = s.segment, 20 | charWidth = stringWidth(char), 21 | encoded = encode(char), 22 | isNarrow = charWidth === 1, 23 | isNone = isNarrow && encoded === 32 && ST.isEmpty(style); 24 | 25 | chunkWidth += charWidth; 26 | 27 | return [ 28 | style, 29 | isNone ? 0 : encoded, 30 | isNone ? 0 : isNarrow ? 1 : 2, 31 | ] as PackedCell; 32 | }).flatMap(packedCell => 33 | packedCell[2] === 2 ? [packedCell, PA.packedCont] : [packedCell], 34 | ); 35 | 36 | return [chunkWidth, res]; 37 | }; 38 | -------------------------------------------------------------------------------- /src/cell/rune.ts: -------------------------------------------------------------------------------- 1 | import { ByteArray } from 'util/array'; 2 | import { Unary } from 'util/function'; 3 | 4 | /** 5 | * #### A unicode code point 6 | * 7 | * Encoded as a 32bit unsigned integer, representing the character code point, 8 | * in little-endian order. 9 | * 10 | * Most characters will have most bits empty. 11 | */ 12 | export type Rune = number; 13 | 14 | const [decoder, encoder] = [new TextDecoder(), new TextEncoder()]; 15 | 16 | /** Encode a single character as a 32bit unsigned int */ 17 | export const encode: Unary = s => { 18 | if (s.length === 0) return 0; 19 | 20 | const bytes = new DataView(encoder.encode(s).buffer); 21 | 22 | return bytes.byteLength === 1 23 | ? bytes.getUint8(0) 24 | : bytes.byteLength === 4 25 | ? bytes.getUint32(0, true) 26 | : bytes.byteLength === 2 27 | ? bytes.getUint16(0, true) 28 | : 2 ** 8 * bytes.getUint16(1, true) + bytes.getUint8(0); 29 | }; 30 | 31 | /** Decode a single unicode code point from a 32bit unsigned int */ 32 | export const decode: Unary = r => { 33 | if (r === 0) return ''; 34 | else if (r < 2 ** 8) return decoder.decode(ByteArray.from([r])); 35 | 36 | const byte4 = (r >> 24) & 0xff; 37 | const byte3 = (r >> 16) & 0x00ff; 38 | const byte2 = (r >> 8) & 0x0000ff; 39 | const byte1 = (r >> 0) & 0x000000ff; 40 | 41 | return decoder.decode( 42 | ByteArray.from( 43 | byte4 !== 0 44 | ? [byte1, byte2, byte3, byte4] 45 | : byte3 !== 0 46 | ? [byte1, byte2, byte3] 47 | : [byte1, byte2], 48 | ), 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/cell/tests/build.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as ST from 'src/style'; 3 | import { plainChar, plainWide } from '../build'; 4 | 5 | suite('grid cell build', () => { 6 | test('plainChar', () => 7 | assert.deepEqual(plainChar('x'), [ST.empty, 'x', 'char'])); 8 | 9 | test('wide', () => 10 | assert.deepEqual(plainWide('🙂'), [ 11 | [ST.empty, '🙂', 'wide'], 12 | [ST.empty, '', 'cont'], 13 | ])); 14 | }); 15 | -------------------------------------------------------------------------------- /src/cell/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { array as AR, function as FN } from 'fp-ts'; 3 | import { mapBoth } from 'fp-ts-std/Tuple'; 4 | import { Pair } from 'util/tuple'; 5 | import * as BU from '../build'; 6 | import { Cell } from '../types'; 7 | import { styleArb } from 'src/style/tests/helpers'; 8 | 9 | export * from 'src/color/tests/helpers'; 10 | export * from 'src/style/tests/helpers'; 11 | 12 | type ArbCells = fc.Arbitrary; 13 | 14 | const emojiK: Pair> = FN.pipe( 15 | ['🙂', '😢'], 16 | mapBoth(fc.constant), 17 | ); 18 | 19 | const wideArb: fc.Arbitrary = fc 20 | .tuple(styleArb, fc.oneof(...emojiK)) 21 | .map(BU.wide); 22 | 23 | const charArb: fc.Arbitrary = fc 24 | .tuple(styleArb, fc.char()) 25 | .map(BU.narrow); 26 | 27 | export const narrowOrNoneArb: fc.Arbitrary = fc.oneof( 28 | charArb, 29 | fc.constant(BU.none), 30 | ); 31 | 32 | export const cellArb: ArbCells = fc.oneof( 33 | narrowOrNoneArb.map(AR.of), 34 | wideArb, 35 | fc.constant([BU.cont]), 36 | ); 37 | -------------------------------------------------------------------------------- /src/cell/tests/instances.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR } from 'fp-ts'; 2 | import * as laws from 'fp-ts-laws'; 3 | import { assert, suite, test } from 'vitest'; 4 | import * as BU from '../build'; 5 | import { eq, show } from '../instances'; 6 | import { cellArb } from './helpers'; 7 | 8 | suite('grid cell instances', () => { 9 | suite('show', () => { 10 | test('none', () => assert.equal(show.show(BU.none), 'none')); 11 | test('char', () => 12 | assert.equal(show.show(BU.plainChar('x')), 'char: “x” style=∅')); 13 | }); 14 | suite('laws', () => test('eq', () => laws.eq(AR.getEq(eq), cellArb))); 15 | }); 16 | -------------------------------------------------------------------------------- /src/cell/tests/ops.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as IUT from '../ops'; 3 | import { Cell } from '../types'; 4 | 5 | suite('grid cell ops', () => { 6 | test('transparent', () => { 7 | const cell: Cell = [[0xff_00_00_00, 0, 1], 'x', 'char']; 8 | const res = IUT.fgOps.transparent(cell); 9 | assert.deepEqual(res, [[0x00_00_00_00, 0, 1], 'x', 'char']); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/cell/tests/packed.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as BU from '../build'; 3 | import * as IUT from '../packed'; 4 | import { Cell } from '../types'; 5 | 6 | suite('grid cell packed', () => { 7 | const testRoundTrip = (name: string, cell: Cell) => { 8 | const buffer = new Uint32Array(4); 9 | IUT.writeCell(buffer)(0, cell); 10 | test(name, () => assert.deepEqual(IUT.readCell(buffer)(0), cell)); 11 | }; 12 | 13 | testRoundTrip('none', BU.none); 14 | 15 | testRoundTrip('one byte narrow char', BU.plainChar('a')); 16 | testRoundTrip('two byte narrow char', BU.plainChar('φ')); 17 | testRoundTrip('three byte narrow char', BU.plainChar('⤖')); 18 | 19 | testRoundTrip( 20 | 'red fg three byte narrow char', 21 | BU.fgChar(0xff_00_00_ff)('⤖')[0], 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /src/cell/tests/rune.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import { decode, encode } from '../rune'; 3 | 4 | suite('grid cell rune', () => { 5 | const testRoundTrip = (name: string, char: string) => 6 | test(`${name}: “${char}”`, () => assert.equal(decode(encode(char)), char)); 7 | 8 | testRoundTrip('empty', ''); 9 | testRoundTrip('1 byte', 'a'); 10 | testRoundTrip('2 byte', 'φ'); 11 | testRoundTrip('3 byte', '⤖'); 12 | testRoundTrip('4 byte', '🙂'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/cell/tests/types.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as ST from 'src/style'; 3 | import * as TY from '../types'; 4 | import { Cell } from '../types'; 5 | 6 | const cell: Cell = [ST.empty, '', 'none']; 7 | 8 | suite('grid cell types', () => { 9 | test('empty cell has empty deco', () => assert.equal(TY.getDeco(cell), 0)); 10 | test('setDeco', () => assert.equal(TY.getDeco(TY.setDeco(1)(cell)), 1)); 11 | }); 12 | -------------------------------------------------------------------------------- /src/cell/type.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { indexRecord } from 'util/array'; 3 | import { Unary } from 'util/function'; 4 | import { typedKeys } from 'util/object'; 5 | 6 | /** 7 | * Cell types: 8 | * 9 | * 1. `none` represents an empty cell 10 | * 2. `char` represents a character of width 1 11 | * 3. `wide` represents 1st cell of a character of width > 1 12 | * 4. `cont` represents a continuation of a previous wide character 13 | * 14 | */ 15 | export type CellType = keyof typeof index; 16 | export type PackedType = number; 17 | 18 | export const width = { 19 | none: 0, 20 | char: 1, 21 | wide: 2, 22 | cont: 0, 23 | } as const; 24 | 25 | export const index = FN.pipe(width, typedKeys, indexRecord); 26 | 27 | export const types: CellType[] = typedKeys(index); 28 | 29 | export const matchPackedType = 30 | (onNone: R, onChar: R, onWide: R, onCont: R): Unary => 31 | packedType => 32 | packedType === 0 33 | ? onNone 34 | : packedType === 1 35 | ? onChar 36 | : packedType === 2 37 | ? onWide 38 | : onCont; 39 | 40 | export const matchCellType = 41 | (onNone: R, onChar: R, onWide: R, onCont: R): Unary => 42 | type => 43 | FN.pipe(index[type], matchPackedType(onNone, onChar, onWide, onCont)); 44 | 45 | export const pack: Unary = type => index[type], 46 | unpack: Unary = idx => types[idx]; 47 | 48 | export const runeWidth: Unary = t => width[unpack(t)]; 49 | -------------------------------------------------------------------------------- /src/cell/types.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { Color } from 'src/color'; 3 | import { Endo, Unary } from 'util/function'; 4 | import * as style from 'src/style'; 5 | import { Deco, Style } from 'src/style'; 6 | import * as TY from './type'; 7 | import { CellType } from './type'; 8 | 9 | export type Cell = [style: Style, rune: string, type: CellType]; 10 | 11 | export const [getStyle, getFg, getBg, getDeco, getRune, getCellType]: [ 12 | Unary, 13 | Unary, 14 | Unary, 15 | Unary, 16 | Unary, 17 | Unary, 18 | ] = [c => c[0], c => c[0][0], c => c[0][1], c => c[0][2], c => c[1], c => c[2]]; 19 | 20 | export const [setStyle, setFg, setBg, setDeco, setRune, setCellType]: [ 21 | Unary>, 22 | Unary>, 23 | Unary>, 24 | Unary>, 25 | Unary>, 26 | Unary>, 27 | ] = [ 28 | st => 29 | ([, ...rest]) => 30 | [st, ...rest], 31 | cb => 32 | ([st, ...rest]) => 33 | [style.setFg(cb)(st), ...rest], 34 | cb => 35 | ([st, ...rest]) => 36 | [style.setBg(cb)(st), ...rest], 37 | deco => 38 | ([st, ...rest]) => 39 | [style.setDeco(deco)(st), ...rest], 40 | char => 41 | ([st, , type]) => 42 | [st, char, type], 43 | type => 44 | ([st, deco]) => 45 | [st, deco, type], 46 | ]; 47 | 48 | export const matchCell = 49 | ( 50 | onNone: R, 51 | onChar: Unary, 52 | onWide: Unary, 53 | onCont: R, 54 | ): Unary => 55 | cell => 56 | FN.pipe( 57 | cell, 58 | getCellType, 59 | TY.matchCellType(onNone, onChar(cell), onWide(cell), onCont), 60 | ); 61 | -------------------------------------------------------------------------------- /src/color.ts: -------------------------------------------------------------------------------- 1 | export * from './color/color'; 2 | -------------------------------------------------------------------------------- /src/color/color.ts: -------------------------------------------------------------------------------- 1 | export * from './ops'; 2 | export * from './types'; 3 | export * from './lens'; 4 | export * from './opacity'; 5 | export * from './paint'; 6 | export * from './palette'; 7 | export * from './blend'; 8 | export * from './instances'; 9 | 10 | export type { 11 | Rgba, 12 | ColorBin, 13 | RgbChannel, 14 | Channel, 15 | RgbTuple, 16 | RgbaTuple, 17 | } from './rgba'; 18 | 19 | export type { HslChannel, HslaChannel, Hsl } from './hsla'; 20 | -------------------------------------------------------------------------------- /src/color/instances.ts: -------------------------------------------------------------------------------- 1 | import { eq as EQ, function as FN, monoid as MO, show as SH } from 'fp-ts'; 2 | import { mapBoth } from 'fp-ts-std/Tuple'; 3 | import { Unary } from 'util/function'; 4 | import { Pair } from 'util/tuple'; 5 | import { BlendMode, blend } from './blend'; 6 | import { toRgbaColor, normalize, Color } from './types'; 7 | import { isEmpty } from './ops'; 8 | 9 | export const eq: EQ.Eq = { 10 | equals: (a, b) => normalize(a) === normalize(b), 11 | }; 12 | 13 | export const show: SH.Show = { 14 | show: c => { 15 | if (isEmpty(c)) return 'none'; 16 | const { r, g, b, a } = toRgbaColor(c); 17 | return [`R:${r}`, `G:${g}`, `B:${b}`, `A:${a.toFixed(2)}`].join(' '); 18 | }, 19 | }; 20 | 21 | export const getMonoid: Unary> = mode => ({ 22 | empty: 0, 23 | concat: FN.pipe(mode, blend, FN.untupled), 24 | }); 25 | 26 | export const [overMonoid, underMonoid]: Pair> = FN.pipe( 27 | ['combineOver', 'combineUnder'], 28 | mapBoth(getMonoid), 29 | ); 30 | -------------------------------------------------------------------------------- /src/color/opacity.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { Unary } from 'util/function'; 3 | import { FromKeys, fromKeys, mapValues, typedKeys } from 'util/object'; 4 | import { Tuple4 } from 'util/tuple'; 5 | 6 | /** 7 | * A record of named opacity levels. 8 | * 9 | * The values are floats in range of 0-1. Higher values mean _more opaque_. 10 | */ 11 | export const levels = FN.pipe( 12 | { 13 | opaque: 1, 14 | semiOpaque: 0.6, 15 | semiTransparent: 0.2, 16 | transparent: 0, 17 | }, 18 | mapValues(o => Math.floor(o * 255)), 19 | ); 20 | 21 | /** A named opacity level. */ 22 | export type Level = keyof typeof levels; 23 | 24 | export const levelNames: Tuple4 = typedKeys(levels); 25 | 26 | /** 27 | * Given an opacity level, return the opacity level as a number between 0 and 255. 28 | */ 29 | export const levelAt: Unary = l => levels[l]; 30 | 31 | /** 32 | * Given a function of type `OpacityLevel ⇒ R`, will return a record of 33 | * `OpacityLevel` to `R`. 34 | */ 35 | export const opacityRecord: FromKeys = fromKeys(levelNames); 36 | -------------------------------------------------------------------------------- /src/color/paint.ts: -------------------------------------------------------------------------------- 1 | import ansis, { Ansis } from 'ansis'; 2 | import { function as FN } from 'fp-ts'; 3 | import { Endo, Unary } from 'util/function'; 4 | import { Pair } from 'util/tuple'; 5 | import { Color, toRgbaColor } from './types'; 6 | import { grays } from './palette'; 7 | 8 | /** Colorize a string with the given color */ 9 | export const fg: Unary> = c => { 10 | const { r, g, b, a } = toRgbaColor(c); 11 | return a === 0 ? FN.identity : ansis.rgb(r, g, b); 12 | }; 13 | 14 | /** Colorize the given string background with the given color */ 15 | export const bg: Unary> = c => { 16 | const { r, g, b, a } = toRgbaColor(c); 17 | return a === 0 ? FN.identity : ansis.bgRgb(r, g, b); 18 | }; 19 | 20 | /** Colorize foreground and background with the given colors */ 21 | export const of: Unary, Endo> = 22 | ([fgColor, bgColor]) => 23 | s => 24 | fg(fgColor)(bg(bgColor)(s)); 25 | 26 | /** Add foreground and background to the given 27 | * [ansis](https://github.com/webdiscus/ansis) object 28 | */ 29 | export const addToAnsis: Unary, Endo> = 30 | ([fgColor, bgColor]) => 31 | ansis => { 32 | const { r: rFg, g: gFg, b: bFg, a: aFg } = toRgbaColor(fgColor), 33 | { r: rBg, g: gBg, b: bBg, a: aBg } = toRgbaColor(bgColor); 34 | 35 | return aBg === 0 36 | ? aFg === 0 37 | ? ansis 38 | : ansis.rgb(rFg, gFg, bFg) 39 | : aFg === 0 40 | ? ansis.bgRgb(rBg, gBg, bBg) 41 | : ansis.rgb(rFg, gFg, bFg).bgRgb(rBg, gBg, bBg); 42 | }; 43 | 44 | export const [grayFg, grayBg]: Pair>> = [ 45 | n => fg(grays[n]), 46 | n => bg(grays[n]), 47 | ]; 48 | -------------------------------------------------------------------------------- /src/color/palette.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, nonEmptyArray as NE } from 'fp-ts'; 2 | import { Unary } from 'util/function'; 3 | import { normalize, Color } from './types'; 4 | 5 | export const grays: Color[] = FN.pipe( 6 | NE.range(0, 100), 7 | AR.map(l => normalize({ h: 0, s: 0, l })), 8 | ); 9 | 10 | export const rainbowN: Unary = n => 11 | FN.pipe( 12 | NE.range(0, n - 1), 13 | AR.map(h => normalize({ h: Math.round((h * 360) / n + 1), s: 100, l: 50 })), 14 | ); 15 | 16 | export const cycle: Unary> = colors => { 17 | let idx = 0; 18 | return () => { 19 | const res = colors[idx++]; 20 | if (idx === colors.length) idx = 0; 21 | return res; 22 | }; 23 | }; 24 | 25 | export const rainbow6 = rainbowN(6), 26 | rainbow8 = rainbowN(8); 27 | 28 | export const rainbow6Gen = () => cycle(rainbow6), 29 | rainbow8Gen = () => cycle(rainbow8); 30 | -------------------------------------------------------------------------------- /src/color/tests/blend.test.ts: -------------------------------------------------------------------------------- 1 | import { Unary } from 'util/function'; 2 | import { suite, test } from 'vitest'; 3 | import { blend, BlendMode } from '../blend'; 4 | import { Color } from '../types'; 5 | import { colorEq } from './helpers'; 6 | 7 | const transparent: Color = { r: 255, g: 0, b: 0, a: 0 }, 8 | semiOpaqueGreen: Color = { r: 0, g: 255, b: 0, a: 0.5 }; 9 | 10 | export const cs: Unary = s => Object.values(s).join(','); 11 | 12 | const checkPair = 13 | (name: string, lower: Color, upper: Color) => 14 | (mode: BlendMode, expect: Color) => 15 | test(name, () => colorEq(blend(mode)([lower, upper]), expect)); 16 | 17 | suite('color blend ops', () => { 18 | suite('transparent', () => { 19 | suite('lower', () => { 20 | const check = checkPair('transparent+red', transparent, 'red'); 21 | check('under', transparent); 22 | check('normal', 'red'); 23 | check('multiply', 'red'); 24 | }); 25 | 26 | suite('upper', () => { 27 | const check = checkPair('red+transparent', 'red', transparent); 28 | check('over', transparent); 29 | check('under', 'red'); 30 | check('multiply', 'red'); 31 | }); 32 | }); 33 | 34 | suite('opaque', () => { 35 | const check = checkPair('red+blue', 'red', 'blue'); 36 | check('over', 'blue'); 37 | check('combineUnder', 'red'); 38 | check('lighten', { r: 255, g: 0, b: 255, a: 1 }); 39 | }); 40 | 41 | suite('opaque + semi opaque', () => { 42 | const check = checkPair('red+semiOpaqueGreen', 'red', semiOpaqueGreen); 43 | check('over', semiOpaqueGreen); 44 | check('difference', { r: 255, g: 128, b: 0, a: 1 }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/color/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from 'colord'; 2 | import * as fc from 'fast-check'; 3 | import { assert } from 'vitest'; 4 | import { ColorBin, RgbaTuple } from '../rgba'; 5 | import { Color, toRgbaColor } from '../types'; 6 | 7 | export const byteArb = fc.nat(2 ** 8 - 1); 8 | 9 | export const realArb = fc.float(0, 1); 10 | 11 | export const colorBinArb: fc.Arbitrary = fc.nat(2 ** 32 - 1); 12 | 13 | export const rgbaColorArb: fc.Arbitrary = fc.record({ 14 | r: byteArb, 15 | g: byteArb, 16 | b: byteArb, 17 | a: realArb, 18 | }); 19 | 20 | export const rgbaTupleArb: fc.Arbitrary = fc.tuple( 21 | byteArb, 22 | byteArb, 23 | byteArb, 24 | byteArb, 25 | ); 26 | 27 | export const hueArb: fc.Arbitrary = fc.nat(360); 28 | 29 | export const colorEq = (a: Color, b: Color) => 30 | assert.deepEqual(toRgbaColor(a), toRgbaColor(b)); 31 | -------------------------------------------------------------------------------- /src/color/tests/instances.test.ts: -------------------------------------------------------------------------------- 1 | import * as laws from 'fp-ts-laws'; 2 | import { assert, suite, test } from 'vitest'; 3 | import * as color from '../color'; 4 | import { colorBinArb, rgbaColorArb } from './helpers'; 5 | 6 | suite('color instances', () => { 7 | test('show', () => 8 | assert.equal( 9 | color.show.show(color.fromName('red')), 10 | 'R:255 G:0 B:0 A:1.00', 11 | )); 12 | 13 | suite('laws', () => { 14 | test('eq', () => laws.eq(color.eq, rgbaColorArb)); 15 | 16 | test('over monoid', () => 17 | laws.monoid(color.overMonoid, color.eq, colorBinArb)); 18 | 19 | test('under monoid', () => 20 | laws.monoid(color.underMonoid, color.eq, colorBinArb)); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/color/tests/lens.test.ts: -------------------------------------------------------------------------------- 1 | import { HslColor, RgbColor } from 'colord'; 2 | import * as fc from 'fast-check'; 3 | import { function as FN } from 'fp-ts'; 4 | import { testProp } from 'util/test'; 5 | import { assert, suite, test } from 'vitest'; 6 | import * as IUT from '../lens'; 7 | import { byteArb, colorEq, rgbaTupleArb } from './helpers'; 8 | 9 | const yellowHsl: HslColor = { h: 60, s: 100, l: 50 }; 10 | 11 | const yellowRgb: RgbColor = { r: 255, g: 255, b: 0 }; 12 | 13 | suite('lens', () => { 14 | suite('l', () => { 15 | test('get', () => assert.equal(IUT.l.get('yellow'), 50)); 16 | 17 | test('set', () => colorEq(IUT.l.set(100)('yellow'), 'white')); 18 | }); 19 | 20 | suite('hue', () => { 21 | test('get', () => assert.equal(IUT.h.get(yellowRgb), 60)); 22 | 23 | test('set.get', () => 24 | assert.equal(FN.pipe([0, 100, 50, 0], IUT.h.set(1), IUT.h.get), 1)); 25 | }); 26 | 27 | suite('hsl', () => { 28 | test('get', () => 29 | assert.deepEqual(IUT.hsl.get({ ...yellowHsl, a: 0 }), yellowHsl)); 30 | 31 | test('set.get', () => 32 | assert.equal(FN.pipe([0, 100, 50, 0], IUT.h.set(1), IUT.h.get), 1)); 33 | }); 34 | 35 | testProp( 36 | '∀c ∈ RgbaTuple, ∀b ∈ byte: b ‣ g.set(c) ‣ g.get = b', 37 | [fc.tuple(byteArb, rgbaTupleArb)], 38 | ([cur, orig]) => { 39 | const actual = FN.pipe(orig, IUT.g.set(cur), IUT.g.get); 40 | assert.equal(actual, cur); 41 | }, 42 | ); 43 | }); 44 | -------------------------------------------------------------------------------- /src/color/tests/ops.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as color from '../color'; 3 | import { colorEq } from './helpers'; 4 | 5 | suite('color ops', () => { 6 | suite('colord ops', () => { 7 | test('invert', () => colorEq(color.invert('black'), 'white')); 8 | 9 | test('rotate', () => 10 | colorEq(color.rotate(90)({ h: 90, s: 100, l: 50 }), { 11 | h: 180, 12 | s: 100, 13 | l: 50, 14 | a: 1, 15 | })); 16 | 17 | test('hue', () => colorEq(color.hue(0)('yellow'), 'red')); 18 | }); 19 | 20 | suite('opacity', () => { 21 | suite('opaque', () => { 22 | const opaque = color.opaque(0); 23 | 24 | test('“a” lens', () => assert.equal(color.a.get(opaque), 255)); 25 | 26 | test('“opacity” lens', () => assert.equal(color.opacity.get(opaque), 1)); 27 | }); 28 | 29 | suite('semiTransparent', () => { 30 | const semi = color.semiTransparent('yellow'); 31 | 32 | test('“a” lens', () => assert.equal(color.a.get(semi), 51)); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/color/tests/paint.test.ts: -------------------------------------------------------------------------------- 1 | import ansis from 'ansis'; 2 | import { function as FN } from 'fp-ts'; 3 | import { assert, suite, test } from 'vitest'; 4 | import { fg, of } from '../paint'; 5 | 6 | suite('color paint', () => { 7 | test('empty', () => assert.equal(FN.pipe('foo', fg(0)), 'foo')); 8 | 9 | test('transparent', () => assert.equal(FN.pipe('foo', of([0, 0])), 'foo')); 10 | 11 | test('yellow', () => 12 | assert.equal( 13 | FN.pipe('foo', fg(0xff_00_ff_ff)), 14 | ansis.hex('#ffff00')('foo'), 15 | )); 16 | }); 17 | -------------------------------------------------------------------------------- /src/color/tests/types.test.ts: -------------------------------------------------------------------------------- 1 | import { HslColor, RgbColor } from 'colord'; 2 | import { typedValues } from 'util/object'; 3 | import { suite, test } from 'vitest'; 4 | import * as IUT from '../types'; 5 | import { colorEq } from './helpers'; 6 | 7 | const yellow: IUT.Color = 0xff_00_ff_ff, 8 | yellowHsl: HslColor = { h: 60, s: 100, l: 50 }, 9 | yellowRgb: RgbColor = { r: 255, g: 255, b: 0 }; 10 | 11 | suite('color types', () => { 12 | suite('normalize', () => { 13 | test('bin', () => colorEq(yellow, yellowRgb)); 14 | 15 | test('named', () => colorEq('yellow', yellow)); 16 | 17 | test('rgba', () => colorEq(yellowRgb, yellowHsl)); 18 | 19 | test('rgb tuple', () => colorEq(typedValues(yellowRgb), yellow)); 20 | 21 | test('hsla', () => colorEq({ ...yellowHsl, a: 255 }, yellow)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/devBin/all-combinations.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, option as OP } from 'fp-ts'; 2 | import { toSnd } from 'fp-ts-std/Tuple'; 3 | import { makeRegistry as makeBitmapRegistry } from 'src/bitmap/registry'; 4 | import { glyph } from 'src/stacka'; 5 | import { computePairs } from 'src/glyph/relation'; 6 | import { Pair } from 'util/tuple'; 7 | 8 | const bitmapRegistry = makeBitmapRegistry(); 9 | 10 | const [charPairs] = computePairs(bitmapRegistry); 11 | 12 | const combined: [Pair, OP.Option][] = FN.pipe( 13 | charPairs, 14 | FN.pipe(glyph.tryCombine, toSnd, AR.map), 15 | ); 16 | 17 | const res = FN.pipe( 18 | combined, 19 | AR.map(([[fst, snd], resOp]) => 20 | FN.pipe( 21 | resOp, 22 | OP.chain(res => 23 | fst !== snd && fst !== res && snd !== res 24 | ? OP.some(`${fst} ⊕ ${snd} = ${res}`) 25 | : OP.none, 26 | ), 27 | ), 28 | ), 29 | AR.compact, 30 | AR.chunksOf(7), 31 | ); 32 | 33 | for (const chunk of res) { 34 | console.log(chunk.join(' ')); 35 | console.log(); 36 | } 37 | -------------------------------------------------------------------------------- /src/devBin/precompute-bitmaps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makeRegistry as makeBitmapRegistry, 3 | Registry as BitmapRegistry, 4 | } from 'src/bitmap/registry'; 5 | 6 | type BitmapMaps = 7 | | 'matrixByChar' 8 | | 'charByMatrix' 9 | | 'rolesByChar' 10 | | 'charsByRole'; 11 | 12 | type MapOf = BitmapRegistry[M]; 13 | type EntryOf = [ 14 | MapOf extends Map ? K : never, 15 | MapOf extends Map ? V : never, 16 | ]; 17 | 18 | const getBitmaps = (bitmapReg: BitmapRegistry) => { 19 | const entries = (m: M): EntryOf[] => 20 | Array.from(bitmapReg[m].entries() as IterableIterator>); 21 | 22 | return { 23 | chars: bitmapReg.chars, 24 | roles: bitmapReg.roles, 25 | matrixByChar: entries('matrixByChar'), 26 | charByMatrix: entries('charByMatrix'), 27 | rolesByChar: entries('rolesByChar'), 28 | charsByRole: entries('charsByRole'), 29 | }; 30 | }; 31 | 32 | const bitmapRegistry = makeBitmapRegistry(); 33 | 34 | const bitmaps = getBitmaps(bitmapRegistry); 35 | 36 | console.log(JSON.stringify({ bitmaps })); 37 | -------------------------------------------------------------------------------- /src/devBin/pure.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple pure unary function as an example implementation under test 3 | */ 4 | 5 | import { apply0, Unary } from 'util/function'; 6 | 7 | export const toBinary: Unary = n => { 8 | const f = () => n.toString(2); 9 | return apply0(f); 10 | }; 11 | -------------------------------------------------------------------------------- /src/devBin/should-import.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { toBinary } from 'src/devBin/pure'; 4 | 5 | /** 6 | * Run as part of the self-test to ensure project configuration allows import 7 | * of typescript files and the path mapping works. 8 | */ 9 | 10 | console.log(toBinary(14)); 11 | -------------------------------------------------------------------------------- /src/devBin/should-run.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | export {}; 4 | 5 | /** 6 | * Run as part of the self-test to ensure project configuration allows running 7 | * of typescript scripts. 8 | */ 9 | 10 | console.log('Hello world'); 11 | -------------------------------------------------------------------------------- /src/geometry/corner.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, readonlyArray as RA, show as SH } from 'fp-ts'; 2 | import { dup } from 'fp-ts-std/Tuple'; 3 | import { Unary } from 'util/function'; 4 | import { Tuple4 } from 'util/tuple'; 5 | 6 | export const all = [ 7 | 'topLeft', 8 | 'topRight', 9 | 'bottomLeft', 10 | 'bottomRight', 11 | ] as const; 12 | 13 | export type Corner = typeof all[number]; 14 | 15 | export type Cornered = Record; 16 | 17 | export type Corners = Cornered; 18 | 19 | export const sym: Corners = { 20 | topLeft: '↖', 21 | topRight: '↗', 22 | bottomLeft: '↙', 23 | bottomRight: '↘', 24 | }; 25 | 26 | export const value = FN.pipe(all, RA.map(dup), Object.fromEntries) as { 27 | [K in Corner]: K; 28 | }; 29 | 30 | export const check = (d: string): d is Corner => d in sym; 31 | 32 | export const [map, zip] = [ 33 | (f: Unary) => FN.pipe(all, RA.map(f)) as Tuple4, 34 | (r: Tuple4) => FN.pipe(all, RA.zip(r)) as Tuple4<[Corner, R]>, 35 | ]; 36 | 37 | export const fromTuple = ([ 38 | topLeft, 39 | topRight, 40 | bottomLeft, 41 | bottomRight, 42 | ]: Tuple4): Cornered => ({ 43 | topLeft, 44 | topRight, 45 | bottomLeft, 46 | bottomRight, 47 | }); 48 | 49 | export const singleton = (t: T): Cornered => fromTuple([t, t, t, t]); 50 | 51 | export const show: SH.Show = { show: corner => sym[corner] }; 52 | -------------------------------------------------------------------------------- /src/geometry/orientation.ts: -------------------------------------------------------------------------------- 1 | import { picksT } from 'util/object'; 2 | import { Pair } from 'util/tuple'; 3 | 4 | export type Orientation = 'horizontal' | 'vertical'; 5 | export type Oriented = Record; 6 | export type Orient = Oriented; 7 | 8 | export const orientations: Pair = ['horizontal', 'vertical']; 9 | 10 | /** 11 | * Filter out all entries except those keyed by `horizontal` or `vertical` 12 | */ 13 | export const pickOrientations = 14 | () => 15 | >(d: D): Oriented => 16 | picksT(orientations)(d); 17 | -------------------------------------------------------------------------------- /src/geometry/spacing.ts: -------------------------------------------------------------------------------- 1 | import { 2 | eq as EQ, 3 | function as FN, 4 | monoid as MO, 5 | number as NU, 6 | show as SH, 7 | } from 'fp-ts'; 8 | import * as LE from 'monocle-ts/Lens'; 9 | import { ModLens, modLens } from 'util/lens'; 10 | import { Unary } from 'util/function'; 11 | import { Pair, Tuple4 } from 'util/tuple'; 12 | import { Dir } from './dir'; 13 | 14 | export type Spacing = Record; 15 | 16 | export const build = ( 17 | top: number, 18 | right: number, 19 | bottom: number, 20 | left: number, 21 | ) => ({ 22 | top, 23 | right, 24 | bottom, 25 | left, 26 | }), 27 | tupled = FN.tupled(build), 28 | rect: Unary, Spacing> = ([h, v]) => build(v, h, v, h), 29 | square: Unary = n => rect([n, n]), 30 | fromTop: Unary = n => build(n, 0, 0, 0), 31 | fromRight: Unary = n => build(0, n, 0, 0), 32 | fromLeft: Unary = n => build(0, 0, n, 0), 33 | fromBottom: Unary = n => build(0, 0, 0, n), 34 | empty = build(0, 0, 0, 0); 35 | 36 | const [nm, ne] = [NU.MonoidSum, NU.Eq]; 37 | 38 | export const monoid: MO.Monoid = MO.struct({ 39 | top: nm, 40 | right: nm, 41 | bottom: nm, 42 | left: nm, 43 | }); 44 | 45 | export const eq: EQ.Eq = EQ.struct({ 46 | top: ne, 47 | right: ne, 48 | bottom: ne, 49 | left: ne, 50 | }); 51 | 52 | export const show: SH.Show = { 53 | show: ({ top, right, bottom, left }) => `“${top}.${right}.${bottom}.${left}”`, 54 | }; 55 | 56 | const lens = LE.id(); 57 | 58 | export const [top, right, bottom, left]: Tuple4> = [ 59 | FN.pipe(lens, LE.prop('top'), modLens), 60 | FN.pipe(lens, LE.prop('right'), modLens), 61 | FN.pipe(lens, LE.prop('bottom'), modLens), 62 | FN.pipe(lens, LE.prop('left'), modLens), 63 | ]; 64 | -------------------------------------------------------------------------------- /src/geometry/tests/borderDir.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { borderDir } from 'src/geometry'; 3 | import { assert, suite, test } from 'vitest'; 4 | 5 | const show = borderDir.show.show; 6 | 7 | suite('borderDir', () => { 8 | test('basic', () => assert.equal(borderDir.left, 'left')); 9 | 10 | test('show dir', () => assert.equal(show(borderDir.right), '→')); 11 | 12 | test('show corner', () => assert.equal(show(borderDir.bottomLeft), '↙')); 13 | 14 | test('snugBorderDirs', () => 15 | assert.deepEqual( 16 | FN.pipe([...borderDir.snugBorderDirs('top')], AR.map(show)), 17 | ['↖', '↑', '↗'], 18 | )); 19 | 20 | test('snugCorners', () => 21 | assert.deepEqual(FN.pipe([...borderDir.snugCorners('top')], AR.map(show)), [ 22 | '↖', 23 | '↗', 24 | ])); 25 | 26 | test('snug', () => 27 | assert.deepEqual(FN.pipe([...borderDir.snug('top')], AR.map(show)), [ 28 | '←', 29 | '↖', 30 | '↑', 31 | '↗', 32 | '→', 33 | ])); 34 | }); 35 | -------------------------------------------------------------------------------- /src/geometry/tests/corner.test.ts: -------------------------------------------------------------------------------- 1 | import { corner } from 'src/geometry'; 2 | import { assert, suite, test } from 'vitest'; 3 | 4 | suite('corner', () => { 5 | test('basic', () => assert.equal(corner.bottomRight, 'bottomRight')); 6 | 7 | test('show', () => assert.equal(corner.show.show(corner.bottomLeft), '↙')); 8 | }); 9 | -------------------------------------------------------------------------------- /src/geometry/tests/dir.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { dir } from 'src/geometry'; 3 | import { assert, suite, test } from 'vitest'; 4 | import { mapBoth } from 'fp-ts-std/Tuple'; 5 | 6 | suite('dir', () => { 7 | test('basic', () => assert.equal(dir.left, 'left')); 8 | 9 | test('show', () => assert.equal(dir.show.show(dir.bottom), '↓')); 10 | 11 | test('snug', () => 12 | assert.deepEqual(FN.pipe('left', dir.snug, mapBoth(dir.show.show)), [ 13 | '↑', 14 | '↓', 15 | ])); 16 | 17 | test('reverse', () => 18 | assert.equal(FN.pipe(dir.top, dir.reversed, dir.show.show), '↓')); 19 | }); 20 | -------------------------------------------------------------------------------- /src/geometry/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { Size } from '../size'; 3 | import { Pos } from '../pos'; 4 | import { Rect } from '../rect'; 5 | 6 | const maxSizeNat = fc.nat(100); 7 | 8 | export const sizeArb: fc.Arbitrary = fc.record({ 9 | width: maxSizeNat, 10 | height: maxSizeNat, 11 | }); 12 | 13 | const maxPosNat = fc.nat(100); 14 | 15 | export const posArb: fc.Arbitrary = fc.record({ 16 | top: maxPosNat, 17 | left: maxPosNat, 18 | }); 19 | 20 | export const rectArb: fc.Arbitrary = fc.record({ 21 | pos: posArb, 22 | size: sizeArb, 23 | zOrder: fc.nat(10), 24 | }); 25 | -------------------------------------------------------------------------------- /src/geometry/tests/pos.test.ts: -------------------------------------------------------------------------------- 1 | import * as laws from 'fp-ts-laws'; 2 | import { pos, size } from 'src/geometry'; 3 | import { assert, suite, test } from 'vitest'; 4 | import { posArb } from './helpers'; 5 | 6 | const show = pos.show.show; 7 | 8 | suite('pos', () => { 9 | test('basic', () => assert.equal(show(pos(1, 2)), '▲1:◀2')); 10 | 11 | test('minPos', () => { 12 | const actual = pos.min([pos(1, 5), pos(3, 2), pos(3, 3)]); 13 | assert.deepEqual(actual, pos(1, 2)); 14 | }); 15 | 16 | suite('translateToPositive', () => { 17 | test('negative', () => { 18 | assert.deepEqual( 19 | pos.translateToPositive([pos(-1, 5), pos(3, -2), pos(0, 0)]), 20 | [pos(0, 7), pos(4, 0), pos(1, 2)], 21 | ); 22 | }); 23 | 24 | test('all positive', () => 25 | assert.deepEqual(pos.translateToPositive([pos(1, 5), pos(3, 2)]), [ 26 | pos(1, 5), 27 | pos(3, 2), 28 | ])); 29 | }); 30 | 31 | suite('rectSize', () => { 32 | test('positive', () => 33 | assert.deepEqual(pos.rectSize([pos(5, 4), pos(1, 2)]), size(3, 5))); 34 | test('negative', () => 35 | assert.deepEqual(pos.rectSize([pos(1, 2), pos(5, 4)]), size(3, 5))); 36 | }); 37 | 38 | test('addSize', () => 39 | assert.deepEqual(pos.addSize(size(3, 5))(pos(1, 2)), pos(5, 4))); 40 | test('subSize', () => 41 | assert.deepEqual(pos.subSize(size(1, 2))(pos(3, 5)), pos(-1, -4))); 42 | 43 | suite('laws', () => { 44 | test('ord', () => laws.ord(pos.ord.left, posArb)); 45 | test('sum monoid', () => laws.monoid(pos.monoid.sum, pos.eq, posArb)); 46 | test('max monoid', () => laws.monoid(pos.monoid.max, pos.eq, posArb)); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/geometry/tests/size.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import * as laws from 'fp-ts-laws'; 3 | import { add } from 'fp-ts-std/Number'; 4 | import { size } from 'src/geometry'; 5 | import { assert, suite, test } from 'vitest'; 6 | import { sizeArb } from './helpers'; 7 | 8 | suite('size', () => { 9 | test('basic', () => assert.deepEqual(size(1, 2), size.tupled([1, 2]))); 10 | 11 | test('lens', () => 12 | assert.deepEqual( 13 | FN.pipe(size.fromHeight(3), size.height.mod(add(4)), size.height.get), 14 | 7, 15 | )); 16 | 17 | test('addSize', () => 18 | assert.deepEqual(FN.pipe(size(1, 2), size.addC(size(3, 4))), size(4, 6))); 19 | 20 | suite('laws', () => { 21 | test('ord', () => laws.ord(size.ord.height, sizeArb)); 22 | test('sum monoid', () => laws.monoid(size.monoid.sum, size.eq, sizeArb)); 23 | test('max monoid', () => laws.monoid(size.monoid.max, size.eq, sizeArb)); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/glyph.ts: -------------------------------------------------------------------------------- 1 | export * from './glyph/glyph'; 2 | -------------------------------------------------------------------------------- /src/glyph/combine/cachedStack.ts: -------------------------------------------------------------------------------- 1 | import { option as OP, function as FN } from 'fp-ts'; 2 | import { Unary } from 'util/function'; 3 | import { Pair } from 'util/tuple'; 4 | import { mapBoth } from 'fp-ts-std/Tuple'; 5 | import { Matrix, bitmap } from 'src/bitmap'; 6 | import * as SK from '../../bitmap/ops/stack'; 7 | import { join } from 'fp-ts-std/Array'; 8 | 9 | const stackCache = new Map>(); 10 | 11 | const makeKey: Unary, string> = FN.flow( 12 | mapBoth(bitmap.toBin), 13 | join(''), 14 | ); 15 | 16 | export const tryStacks: Unary, OP.Option> = bms => { 17 | const maybeOrUndefChar = stackCache.get(makeKey(bms)); 18 | if (maybeOrUndefChar !== undefined) return maybeOrUndefChar; 19 | 20 | const res: OP.Option = SK.tryStacks(bms); 21 | stackCache.set(makeKey(bms), res); 22 | 23 | return res; 24 | }; 25 | -------------------------------------------------------------------------------- /src/glyph/combine/combine.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, option as OP } from 'fp-ts'; 2 | import { bitmap } from 'src/bitmap'; 3 | import { Unary } from 'util/function'; 4 | import { Pair } from 'util/tuple'; 5 | import { tryStacks } from './cachedStack'; 6 | 7 | export const tryCombine: Unary, OP.Option> = pair => 8 | bitmap.hasCharPair(pair) 9 | ? FN.pipe(pair, bitmap.pairCharToMatrix, tryStacks) 10 | : OP.none; 11 | 12 | export const combine = ([below, above]: Pair) => 13 | below === ' ' || below === '' 14 | ? above 15 | : above === ' ' || above === '' 16 | ? below 17 | : FN.pipe( 18 | FN.pipe([below, above], tryCombine), 19 | FN.pipe(above, FN.constant, OP.getOrElse), 20 | ); 21 | -------------------------------------------------------------------------------- /src/glyph/combine/tests/combine.test.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from 'util/function'; 2 | import { assert, suite, test } from 'vitest'; 3 | import { combine } from '../combine'; 4 | 5 | type TestData = `${string} ${string} ${string}`; 6 | 7 | const check = (testData: TestData) => { 8 | const [fst, snd, expect] = testData.split(/\s+/); 9 | const actual = combine([fst, snd]); 10 | test(testData, () => assert.equal(actual, expect)); 11 | }; 12 | 13 | const checks: Effect = tests => tests.forEach(check); 14 | 15 | suite('glyph combine', () => { 16 | suite('thin', () => 17 | checks([ 18 | '┼ ┴ ┼', // 19 | '└ ┌ ├', 20 | ]), 21 | ); 22 | 23 | suite('thick', () => { 24 | checks(['┛ ┗ ┻', '┓ ┏ ┳', '┓ ┛ ┫']); 25 | }); 26 | 27 | suite('thick-thin', () => 28 | checks([ 29 | '┙ ┖ ┹', // 30 | 31 | '┙ ┸ ┹', 32 | 33 | '┙ ┸ ┹', 34 | 35 | '┙ ┚ ┛', 36 | 37 | '┙ ┍ ┿', 38 | 39 | '┙ ┍ ┿', 40 | 41 | '┙ └ ┵', 42 | 43 | '┒ ┚ ┨', 44 | 45 | '┯ ━ ┯', 46 | 47 | '╃ ╅ ╉', 48 | 49 | '│ ━ ┿', 50 | 51 | '┳ │ ╈', 52 | 53 | '┏ └ ┢', 54 | ]), 55 | ); 56 | 57 | suite('halfSolid', () => 58 | checks([ 59 | '▖ ▘ ▌', // 60 | 61 | '▖ ▝ ▞', 62 | 63 | '▗ ▘ ▚', 64 | 65 | '█ █ █', 66 | 67 | '▌ ▝ ▛', 68 | ]), 69 | ); 70 | suite('double', () => 71 | checks([ 72 | '╗ ╔ ╦', // 73 | '╗ ║ ╣', 74 | '═ ║ ╬', 75 | '═ ─ ━', 76 | ]), 77 | ); 78 | 79 | suite('double-single', () => checks(['╜ ╙ ╨', '╖ ║ ╢'])); 80 | }); 81 | -------------------------------------------------------------------------------- /src/glyph/criteria.ts: -------------------------------------------------------------------------------- 1 | export * from './criteria/chain'; 2 | export * from './criteria/char'; 3 | export * from './criteria/types'; 4 | export * from './criteria/dash'; 5 | export * from './criteria/weight'; 6 | export * from './criteria/shift'; 7 | -------------------------------------------------------------------------------- /src/glyph/criteria/char.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, predicate as PRE } from 'fp-ts'; 2 | import { charCriteria } from './types'; 3 | import { Unary } from 'util/function'; 4 | import { split } from 'util/string'; 5 | import { Pair } from 'util/tuple'; 6 | 7 | export const defineChains = (lines: string[]): string[][] => 8 | FN.pipe( 9 | lines, 10 | FN.pipe(/,/, split, AR.chain), 11 | AR.map(s => Array.from(s)), 12 | ); 13 | 14 | export const definePairs = (lines: string[]) => 15 | defineChains(lines) as Pair[]; 16 | 17 | export const isOneOf: Unary>> = 18 | pairs => 19 | ([fst, snd]) => 20 | pairs.includes(fst + snd); 21 | 22 | const codePoint: Unary = c => c.codePointAt(0) ?? 0; 23 | 24 | export const nextCodePointEq = charCriteria( 25 | ([fst, snd]) => codePoint(fst) + 1 === codePoint(snd), 26 | ); 27 | -------------------------------------------------------------------------------- /src/glyph/criteria/dash.ts: -------------------------------------------------------------------------------- 1 | import { fixedCriteria } from './types'; 2 | 3 | export const dash = fixedCriteria([ 4 | ['─╌,╌┄,┄┈,━╍,╍┅,┅┉,│╎,╎┆,┆┊,┃╏,╏┇,┇┋'], 5 | ['─╌┄┈,━╍┅┉,│╎┆┊,┃╏┇┋'], 6 | ]); 7 | -------------------------------------------------------------------------------- /src/glyph/criteria/shift.ts: -------------------------------------------------------------------------------- 1 | import { fixedCriteria } from './types'; 2 | 3 | export const shift = fixedCriteria([ 4 | [ 5 | '▁─,─▔', 6 | 7 | '▏│,│▕', 8 | 9 | '▀━,━▄', 10 | 11 | '▌┃,┃▐', 12 | 13 | '╵╷', 14 | '╴╶', 15 | 16 | '╹╻', 17 | 18 | '▘╹,╹▝', 19 | 20 | '▖╻,╻▗', 21 | 22 | '▘╸,╸▖', 23 | 24 | '▝╺,╺▗', 25 | 26 | '╴-,-╶', 27 | ], 28 | [ 29 | '▁─▔', 30 | 31 | '▏│▕', 32 | 33 | '▀━▄', 34 | 35 | '▌┃▐', 36 | 37 | '╵╷', 38 | '╴╶', 39 | 40 | '╹╻', 41 | 42 | '▘╹▝', 43 | 44 | '▖╻▗', 45 | 46 | '▘╸▖', 47 | 48 | '▝╺▗', 49 | 50 | '╴-╶', 51 | ], 52 | ]); 53 | -------------------------------------------------------------------------------- /src/glyph/criteria/tests/chain.test.ts: -------------------------------------------------------------------------------- 1 | import { Pair } from 'util/tuple'; 2 | import { test, assert, suite } from 'vitest'; 3 | import { extractChains } from '../chain'; 4 | 5 | suite('chain', () => { 6 | test('basic', () => { 7 | const iut: Pair[] = [ 8 | ['a', 'b'], 9 | ['b', 'c'], 10 | ]; 11 | const actual = extractChains(iut); 12 | 13 | assert.deepEqual(actual, [['a', 'b', 'c']]); 14 | }); 15 | 16 | test('symmetric pairs ignored', () => { 17 | const iut: Pair[] = [ 18 | ['a', 'b'], 19 | ['b', 'a'], 20 | ]; 21 | const actual = extractChains(iut); 22 | 23 | assert.deepEqual(actual, [['a', 'b']]); 24 | }); 25 | 26 | test('two chains', () => { 27 | const iut: Pair[] = [ 28 | ['y', 'z'], 29 | ['b', 'c'], 30 | ['a', 'b'], 31 | ['x', 'y'], 32 | ]; 33 | const actual = extractChains(iut); 34 | 35 | assert.deepEqual(actual, [ 36 | ['x', 'y', 'z'], 37 | ['a', 'b', 'c'], 38 | ]); 39 | }); 40 | 41 | test('connect chains', () => { 42 | const iut: Pair[] = [ 43 | ['a', 'b'], 44 | ['d', 'e'], 45 | ['b', 'c'], 46 | ['c', 'd'], 47 | ]; 48 | const actual = extractChains(iut); 49 | 50 | assert.deepEqual(actual, [['a', 'b', 'c', 'd', 'e']]); 51 | }); 52 | 53 | test('two chains starting from same character', () => { 54 | const iut: Pair[] = [ 55 | ['a', 'b'], 56 | ['c', 'd'], 57 | ['a', 'z'], 58 | ['z', 'y'], 59 | ['y', 'x'], 60 | ['b', 'c'], 61 | ]; 62 | const actual = extractChains(iut); 63 | 64 | assert.deepEqual(actual, [ 65 | ['a', 'b', 'c', 'd'], 66 | ['a', 'z', 'y', 'x'], 67 | ]); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/glyph/criteria/types.ts: -------------------------------------------------------------------------------- 1 | import { predicate as PRE } from 'fp-ts'; 2 | import { Unary } from 'util/function'; 3 | import { Pair } from 'util/tuple'; 4 | import { RelCheck } from '../../bitmap'; 5 | import { defineChains, definePairs } from './char'; 6 | 7 | export type CharCheck = PRE.Predicate>; 8 | 9 | export interface MatrixCriteria { 10 | matrixCheck: RelCheck; 11 | } 12 | 13 | export interface CharCriteria { 14 | charCheck: CharCheck; 15 | } 16 | 17 | export interface FixedCriteria { 18 | pairs: Pair[]; 19 | chains: string[][]; 20 | } 21 | 22 | export type Criteria = MatrixCriteria | CharCriteria | FixedCriteria; 23 | 24 | export const matrixCriteria: Unary = matrixCheck => ({ 25 | matrixCheck, 26 | }), 27 | charCriteria: Unary = charCheck => ({ charCheck }), 28 | fixedCriteria: Unary, FixedCriteria> = ([pairs, chains]) => ({ 29 | pairs: definePairs(pairs), 30 | chains: defineChains(chains), 31 | }); 32 | 33 | const isMatrixCriteria = (c: Criteria): c is MatrixCriteria => 34 | 'matrixCheck' in c, 35 | isCharCriteria = (c: Criteria): c is CharCriteria => 'charCheck' in c; 36 | 37 | export const matchCriteria = 38 | ( 39 | matrix: Unary, 40 | char: Unary, 41 | fixed: Unary<[Pair[], string[][]], R>, 42 | ): Unary => 43 | c => 44 | isMatrixCriteria(c) 45 | ? matrix(c.matrixCheck) 46 | : isCharCriteria(c) 47 | ? char(c.charCheck) 48 | : fixed([c.pairs, c.chains]); 49 | -------------------------------------------------------------------------------- /src/glyph/criteria/weight.ts: -------------------------------------------------------------------------------- 1 | import { fixedCriteria } from './types'; 2 | 3 | export const weight = fixedCriteria([ 4 | [ 5 | ' ─,─━, ╴,╴╸, ╶,╶╺', 6 | 7 | ' ▁,▁▂,▂▃,▃▄,▄▅,▅▆,▆▇,▇█', 8 | 9 | ' ┈,┈┉, ╌,╌╍, ┄,┄┅', 10 | 11 | ' │,│┃, ╵,╵╹, ╷,╷╻', 12 | 13 | ' ▏,▏▎,▎▍,▍▌,▌▋,▋▊,▊▉,▉█', 14 | 15 | ' ┊,┊┋, ╎,╎╏, ┆,┆┇', 16 | 17 | '┌┏,┐┓,└┗,┘┛,┴┻,┬┳,├┣,┤┫,┼╋', 18 | 19 | ' ░,░▒,▒▓,▓█', 20 | ], 21 | [ 22 | ' ─━, ╴╸, ╶╺,┈┉,╌╍,┄┅', 23 | 24 | ' │┃, ╵╹, ╷╻,┊┋,╎╏,┆┇', 25 | 26 | ' ▁▂▃▄▅▆▇█, ▏▎▍▌▋▊▉█', 27 | 28 | ' ░▒▓█', 29 | 30 | '┌┏,┐┓,└┗,┘┛', 31 | 32 | '┴┻,┬┳,├┣,┤┫,┼╋', 33 | ], 34 | ]); 35 | -------------------------------------------------------------------------------- /src/glyph/glyph.ts: -------------------------------------------------------------------------------- 1 | import * as combine from './combine/combine'; 2 | import * as registry from './registry'; 3 | import * as lens from './lens'; 4 | import * as relation from './relation'; 5 | 6 | export type { CharRelation, Relation, RelationName } from './relation'; 7 | export type { MaybeGlyph, Glyph, GlyphRelation, GlyphRelations } from './types'; 8 | 9 | export const glyphOrSpace = registry.glyphOrSpace; 10 | 11 | const fns = { 12 | ...registry, 13 | ...combine, 14 | ...lens, 15 | ...relation, 16 | registry, 17 | } as const; 18 | 19 | export type glyph = typeof glyphOrSpace & typeof fns; 20 | 21 | export const glyph = glyphOrSpace as glyph; 22 | 23 | Object.assign(glyph, fns); 24 | -------------------------------------------------------------------------------- /src/glyph/relation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ord as OD, 3 | function as FN, 4 | array as AR, 5 | nonEmptyArray as NE, 6 | eq as EQ, 7 | number as NU, 8 | } from 'fp-ts'; 9 | import { Unary } from 'util/function'; 10 | 11 | export * from './relation/types'; 12 | export * from './relation/build'; 13 | export * from './relation/defs'; 14 | export * from './relation/create'; 15 | 16 | const ordSize: OD.Ord = FN.pipe( 17 | NU.Ord, 18 | OD.contramap(AR.size), 19 | ), 20 | eqSize: EQ.Eq = FN.pipe( 21 | NU.Eq, 22 | EQ.contramap(AR.size), 23 | ); 24 | 25 | export const chainsBySize: Unary = FN.flow( 26 | AR.sort(ordSize), 27 | NE.group(eqSize), 28 | ); 29 | -------------------------------------------------------------------------------- /src/glyph/relation/build.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, array as AR } from 'fp-ts'; 2 | import { pluck, typedFromEntries } from 'util/object'; 3 | import { BinaryC, Unary } from 'util/function'; 4 | import { Pair } from 'util/tuple'; 5 | import { toFst, toSnd } from 'fp-ts-std/Tuple'; 6 | import { extractChains } from '../criteria'; 7 | import { relationCounts } from './counts'; 8 | import { 9 | CharRelations, 10 | CharRelation, 11 | Relation, 12 | RelationDef, 13 | RelationName, 14 | RelationOf, 15 | } from './types'; 16 | 17 | export const buildWithChains: BinaryC< 18 | RelationDef, 19 | [Pair[], string[][]], 20 | Relation 21 | > = 22 | def => 23 | ([pairs, chains]) => ({ 24 | def, 25 | pairs, 26 | chains, 27 | counts: relationCounts(pairs, chains), 28 | }); 29 | 30 | export const buildRelation: BinaryC< 31 | RelationDef, 32 | Pair[], 33 | Relation 34 | > = def => FN.flow(toSnd(extractChains), buildWithChains(def)); 35 | 36 | export const buildCharRelation = 37 | (on: string, relation: RelationName): Unary, CharRelation> => 38 | ([prev, next]) => ({ 39 | on, 40 | relation, 41 | prev, 42 | next, 43 | }); 44 | 45 | // : 46 | export const buildRelationsOf = ( 47 | rel: RelationOf[], 48 | ): Record> => 49 | FN.pipe(rel, AR.map(toFst(pluck('relation'))), typedFromEntries); 50 | 51 | export const buildCharRelations: Unary = 52 | buildRelationsOf; 53 | -------------------------------------------------------------------------------- /src/glyph/relation/defs.ts: -------------------------------------------------------------------------------- 1 | import { record as RC } from 'fp-ts'; 2 | import { bitmap } from 'src/bitmap'; 3 | import { dash, matrixCriteria, weight, shift } from '../criteria'; 4 | 5 | export const relations = { 6 | turn: { 7 | label: 'Clockwise turn 90ᵒ', 8 | note: '∀ g⯈* ∈ turn : turn(turn(g))=g' + ' ∨ turn(turn(turn(turn(g))))=g', 9 | criteria: matrixCriteria(bitmap.query.turnEq), 10 | }, 11 | 12 | invert: { 13 | label: 'Invert every pixel', 14 | note: 'Symmetric', 15 | criteria: matrixCriteria(bitmap.query.invertEq), 16 | }, 17 | 18 | hFlip: { 19 | label: 'Flip on vertical axis', 20 | criteria: matrixCriteria(bitmap.query.hFlipEq), 21 | }, 22 | 23 | vFlip: { 24 | label: 'Flip on horizontal axis', 25 | criteria: matrixCriteria(bitmap.query.vFlipEq), 26 | }, 27 | 28 | weight: { 29 | label: 'Glyph weight', 30 | criteria: weight, 31 | }, 32 | 33 | shift: { 34 | label: 'Glyph translates top→down/left→right', 35 | criteria: shift, 36 | }, 37 | 38 | dash: { 39 | label: 'Increase in dashing level', 40 | criteria: dash, 41 | }, 42 | } as const; 43 | 44 | export const allRelationNames = RC.keys(relations); 45 | -------------------------------------------------------------------------------- /src/glyph/relation/link.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { buildCharRelation } from 'src/glyph/relation/build'; 3 | import { withAdjacent } from 'util/array'; 4 | import { BinaryC } from 'util/function'; 5 | import { CharRelation, Relation } from './types'; 6 | 7 | /** Create all the `CharacterRelation`s for a relation */ 8 | export const linkChains: BinaryC = 9 | relation => chars => { 10 | const prevMap = new Map(), 11 | nextMap = new Map(); 12 | 13 | for (const chain of relation.chains) { 14 | for (const [char, [prev, next]] of withAdjacent(chain)) { 15 | if (prev.length) 16 | prevMap.set(char, [...(prevMap.get(char) ?? []), ...prev]); 17 | if (next.length) 18 | nextMap.set(char, [...(nextMap.get(char) ?? []), ...next]); 19 | } 20 | } 21 | 22 | return FN.pipe( 23 | chars, 24 | AR.map(char => 25 | buildCharRelation( 26 | char, 27 | relation.def.name, 28 | )([prevMap.get(char) ?? [], nextMap.get(char) ?? []]), 29 | ), 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/glyph/relation/types.ts: -------------------------------------------------------------------------------- 1 | import { Pair } from 'util/tuple'; 2 | import { Criteria } from '../criteria'; 3 | import { Counts } from './counts'; 4 | import { allRelationNames, relations } from './defs'; 5 | 6 | export type RelationName = typeof allRelationNames[number]; 7 | export type RelationNamed = typeof relations[N]; 8 | 9 | export interface RelationDef { 10 | name: RelationName; 11 | label?: string; 12 | note?: string; 13 | /** Relation membership criteria */ 14 | criteria: Criteria; 15 | } 16 | 17 | /** 18 | * A binary relation between a pair of glyphs, defined by some 19 | * `RelationDef` 20 | */ 21 | export interface Relation { 22 | def: RelationDef; 23 | /** Relation defined by its pairs, other fields are computed */ 24 | pairs: Pair[]; 25 | /** Chains are constructed from transitive relations */ 26 | chains: string[][]; 27 | /** Relation statistics */ 28 | counts: Counts; 29 | } 30 | 31 | export interface RelationOf { 32 | on: T; 33 | relation: RelationName; 34 | /** All `prev` links in this relation, for this `T` */ 35 | prev: T[]; 36 | /** All `next` links in this relation, for this `T` */ 37 | next: T[]; 38 | } 39 | 40 | /** 41 | * A character in a relation. 42 | * 43 | * There is one per `(relation count ˣ char membership in relation)`. 44 | * 45 | * If the character appears in more than one chain in the relation, then 46 | * `prev.length/next.length` could be greater than 1. 47 | */ 48 | export type CharRelation = RelationOf; 49 | 50 | /** 51 | * Maps the relation names where a character appears to the `CharRelation` 52 | * of the character and the relation. 53 | * 54 | * There is one `CharRelations` per character. This is relation info we have for 55 | * a character. 56 | */ 57 | export type CharRelations = Record; 58 | -------------------------------------------------------------------------------- /src/glyph/tests/glyph.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, option as OP } from 'fp-ts'; 2 | import { bitmap } from 'src/bitmap'; 3 | import { glyph } from 'src/glyph'; 4 | import { assert, suite, test } from 'vitest'; 5 | 6 | const char = bitmap.line.bottom; 7 | 8 | suite('glyph', () => { 9 | test('glyphByChar', () => { 10 | const iut = glyph.glyphByChar(char); 11 | 12 | assert.equal( 13 | FN.pipe( 14 | iut, 15 | OP.fold( 16 | () => ' ', 17 | g => g.char, 18 | ), 19 | ), 20 | char, 21 | ); 22 | }); 23 | 24 | test('related', () => 25 | assert.deepEqual(glyph.related(char), ['─', '▕', '▏', '▔', ' ', '▂'])); 26 | }); 27 | -------------------------------------------------------------------------------- /src/grid.ts: -------------------------------------------------------------------------------- 1 | export * from './grid/grid'; 2 | -------------------------------------------------------------------------------- /src/grid/alignGrid.ts: -------------------------------------------------------------------------------- 1 | import { Align } from 'src/align'; 2 | import { Size } from 'src/geometry'; 3 | import { Binary, Endo } from 'util/function'; 4 | import { hAlignRow } from './hAlignRow'; 5 | import * as TY from './types'; 6 | import { Grid } from './types'; 7 | import { vAlignGrid } from './vAlignGrid'; 8 | 9 | /** 10 | * Given a size and alignment, resize and re-align the grid so that 11 | * it is sized and aligned as requested, regardless of the original 12 | * size or alignment 13 | */ 14 | export const alignGrid: Binary> = 15 | ({ horizontal, vertical }, writeSize) => 16 | rawGrid => { 17 | if (TY.isEmpty(rawGrid)) return rawGrid; 18 | 19 | const { height: writeH } = writeSize, 20 | [vAligned] = vAlignGrid(vertical, writeH)(rawGrid); 21 | 22 | const res = TY.sized(writeSize), 23 | align = hAlignRow(horizontal, [vAligned, res]); 24 | 25 | let [readIdx, writeIdx] = [0, 0]; 26 | 27 | for (let y = 0; y < writeH; y++) 28 | [readIdx, writeIdx] = align([readIdx, writeIdx]); 29 | 30 | return res; 31 | }; 32 | -------------------------------------------------------------------------------- /src/grid/expand.ts: -------------------------------------------------------------------------------- 1 | import { Spacing } from 'src/geometry'; 2 | import { Endo, Unary } from 'util/function'; 3 | import * as CE from 'src/cell'; 4 | import * as TY from './types'; 5 | import { Grid } from './types'; 6 | 7 | /** 8 | * Given 4 numbers, expands the grid at the top/right/bottom/left directions by 9 | * that number of cells. The new areas are filled with `none` cells. 10 | * 11 | * 1. All spacing values are integers ≥ 0 12 | * 13 | */ 14 | export const expand: Unary> = 15 | ({ top, right, bottom, left }) => 16 | grid => { 17 | const { width: readWidth, buffer: read } = grid, 18 | [hGrow, vGrow] = [right + left, top + bottom]; 19 | 20 | if (TY.isEmpty(grid)) return TY.sized({ width: hGrow, height: vGrow }); 21 | else if (hGrow === 0 && vGrow === 0) return grid; 22 | 23 | const readHeight = TY.countRows(grid), 24 | [width, height] = [readWidth + hGrow, readHeight + vGrow], 25 | rowWords = width * CE.cellWords, 26 | { buffer: write } = TY.sized({ width, height }), 27 | [leftWords, rightWords] = [CE.cellWords * left, CE.cellWords * right]; 28 | 29 | let [readIdx, writeIdx] = [0, rowWords * top]; 30 | 31 | for (let y = 0; y < readHeight; y++) { 32 | writeIdx += leftWords; 33 | 34 | for (let x = 0; x < readWidth; x++) 35 | [readIdx, writeIdx] = CE.copyCell(read, write, readIdx, writeIdx); 36 | 37 | writeIdx += rightWords; 38 | } 39 | 40 | return { width, buffer: write }; 41 | }; 42 | -------------------------------------------------------------------------------- /src/grid/grid.ts: -------------------------------------------------------------------------------- 1 | export * from './alignGrid'; 2 | export * from './crop'; 3 | export * from './expand'; 4 | export * from './instances'; 5 | export * from './measure'; 6 | export * from './mod'; 7 | export * from './ops'; 8 | export * from './paint'; 9 | export * from './parse'; 10 | export * from './resize'; 11 | export * from './resizeElastic'; 12 | export * from './resizeElastic'; 13 | export * from './shrink'; 14 | export * from './stack'; 15 | export * from './stretch'; 16 | export * from './types'; 17 | -------------------------------------------------------------------------------- /src/grid/hAlignRow.ts: -------------------------------------------------------------------------------- 1 | import { align as AL, HAlign } from 'src/align'; 2 | import * as CE from 'src/cell'; 3 | import { Endo } from 'util/function'; 4 | import { Pair } from 'util/tuple'; 5 | import { hGaps } from './measure'; 6 | import { Grid } from './types'; 7 | 8 | /** 9 | * Horizontally align a row copying from read grid to write grid. 10 | * 11 | * Returns a function from `Pair ⇒ Pair` which takes and returns 12 | * the pair of read/write offsets. 13 | * 14 | */ 15 | export const hAlignRow = ( 16 | hAlign: HAlign, 17 | [readGrid, writeGrid]: Pair, 18 | ): Endo> => { 19 | const gaps = hGaps(readGrid); 20 | 21 | return ([initRead, initWrite]: Pair) => { 22 | const [readLeft, readRight] = gaps(initRead), 23 | [readW, writeW] = [readGrid.width, writeGrid.width]; 24 | 25 | const readBodyW = readW - readLeft - readRight, 26 | writeGaps = writeW - readBodyW, 27 | writeLeft = AL.horizontally(hAlign, writeGaps)[0], 28 | crop = writeGaps >= 0 ? 0 : writeLeft, 29 | writeBodyW = readBodyW + crop; 30 | 31 | const [readIdx, writeIdx] = [ 32 | initRead + (readLeft - crop) * CE.cellWords, 33 | initWrite + Math.max(0, writeLeft * CE.cellWords), 34 | ]; 35 | 36 | CE.copyRow(writeBodyW)( 37 | readGrid.buffer, 38 | writeGrid.buffer, 39 | readIdx, 40 | writeIdx, 41 | ); 42 | 43 | return [initRead + readW * CE.cellWords, initWrite + writeW * CE.cellWords]; 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/grid/paint.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cellWords, 3 | closePrev, 4 | emptyPrev, 5 | isEmptyPrev, 6 | paintWith as paintCellWith, 7 | readPackedCell, 8 | } from 'src/cell'; 9 | import { BinaryC } from 'util/function'; 10 | import { Grid } from './types'; 11 | 12 | export const paintWith: BinaryC = 13 | (fillWith: string) => 14 | ({ width, buffer }) => { 15 | if (buffer.length === 0) return []; 16 | 17 | const rowWords = cellWords * width, 18 | height = buffer.length / rowWords, 19 | rows = Array(height), 20 | read = readPackedCell(buffer), 21 | paintCell = paintCellWith(fillWith); 22 | 23 | let offset = 0; 24 | 25 | for (let y = 0; y < height; y++) { 26 | const row = Array(width); 27 | 28 | let prevStyle = emptyPrev; 29 | 30 | for (let x = 0; x < width; x++) { 31 | const [advance, cell] = read(offset); 32 | 33 | const [res, prev] = paintCell(cell, prevStyle); 34 | 35 | row[x] = res; 36 | prevStyle = prev; 37 | offset += advance; 38 | } 39 | 40 | const suffix = isEmptyPrev(prevStyle) ? '' : closePrev(prevStyle)(''); 41 | rows[y] = row.join('') + suffix; 42 | prevStyle = emptyPrev; 43 | } 44 | 45 | return rows; 46 | }; 47 | 48 | export const paint = paintWith(' '); 49 | -------------------------------------------------------------------------------- /src/grid/resize.ts: -------------------------------------------------------------------------------- 1 | import { Align, matchHAlign as hAlign, matchVAlign as vAlign } from 'src/align'; 2 | import { Size } from 'src/geometry'; 3 | import { BinaryC, Endo } from 'util/function'; 4 | import { halfInt } from 'util/number'; 5 | import { expand } from './expand'; 6 | import { crop } from './crop'; 7 | import * as TY from './types'; 8 | import { Grid } from './types'; 9 | 10 | /** 11 | * Resize the grid to given size expanding/shrinking as required according to 12 | * given alignment 13 | */ 14 | export const resize: BinaryC> = 15 | ({ horizontal, vertical }) => 16 | ({ width, height }) => 17 | grid => { 18 | const readWidth = grid.width, 19 | readHeight = TY.countRows(grid), 20 | [wΔ, hΔ] = [width - readWidth, height - readHeight]; 21 | 22 | if (wΔ === 0 && hΔ === 0) return grid; 23 | 24 | const [[left, right], [top, bottom]] = [ 25 | hAlign([0, wΔ], halfInt(wΔ), [wΔ, 0])(horizontal), 26 | vAlign([0, hΔ], halfInt(hΔ), [hΔ, 0])(vertical), 27 | ]; 28 | 29 | if (wΔ >= 0) 30 | return hΔ >= 0 31 | ? expand({ top, right, bottom, left })(grid) 32 | : expand({ top: 0, right, bottom: 0, left })( 33 | crop({ 34 | top: Math.abs(top), 35 | right: 0, 36 | bottom: Math.abs(bottom), 37 | left: 0, 38 | })(grid), 39 | ); 40 | 41 | return hΔ <= 0 42 | ? crop({ 43 | top: Math.abs(top), 44 | right: Math.abs(right), 45 | bottom: Math.abs(bottom), 46 | left: Math.abs(left), 47 | })(grid) 48 | : expand({ top, right: 0, bottom, left: 0 })( 49 | crop({ 50 | top: 0, 51 | right: Math.abs(right), 52 | bottom: 0, 53 | left: Math.abs(left), 54 | })(grid), 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/grid/resizeElastic.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { Size, size as SZ } from 'src/geometry'; 3 | import { Endo, Unary } from 'util/function'; 4 | import { shrink } from './shrink'; 5 | import { stretch } from './stretch'; 6 | import * as TY from './types'; 7 | import { Grid } from './types'; 8 | 9 | const hStretchVShrink: Unary> = size => grid => 10 | FN.pipe( 11 | grid, 12 | stretch({ 13 | width: size.width, 14 | height: TY.countRows(grid), 15 | }), 16 | shrink({ 17 | width: size.width, 18 | height: size.height, 19 | }), 20 | ), 21 | vStretchHShrink: Unary> = size => grid => 22 | FN.pipe( 23 | grid, 24 | stretch({ 25 | width: grid.width, 26 | height: size.height, 27 | }), 28 | shrink({ 29 | width: size.width, 30 | height: size.height, 31 | }), 32 | ); 33 | 34 | /** Stretch and shrink the grid so that it fits the given size */ 35 | export const resizeElastic: Unary> = size => grid => { 36 | const { width: wΔ, height: hΔ } = SZ.sub(size, TY.size(grid)); 37 | return ( 38 | wΔ >= 0 39 | ? hΔ >= 0 40 | ? stretch 41 | : hStretchVShrink 42 | : hΔ >= 0 43 | ? vStretchHShrink 44 | : shrink 45 | )(SZ.abs(size))(grid); 46 | }; 47 | -------------------------------------------------------------------------------- /src/grid/shrink.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN, nonEmptyArray as NE } from 'fp-ts'; 2 | import { transpose } from 'fp-ts-std/Array'; 3 | import { fork } from 'fp-ts-std/Function'; 4 | import { Size } from 'src/geometry'; 5 | import { addAround, head, init, last, tail } from 'util/array'; 6 | import { Endo, Unary } from 'util/function'; 7 | import { Cell } from 'src/cell'; 8 | import * as TY from './types'; 9 | import { Grid } from './types'; 10 | 11 | const shrinkRow: Unary> = width => row => { 12 | if (width === 0 || row.length === 0) return []; 13 | 14 | const gridWidth = row.length; 15 | 16 | if (gridWidth - width === 0) return row; 17 | 18 | const [begin, end] = FN.pipe(row, fork([head, last])); 19 | 20 | return width === 1 21 | ? [begin] 22 | : gridWidth === 1 23 | ? [begin, end] 24 | : FN.pipe( 25 | FN.pipe( 26 | NE.range(0, width - 1), 27 | AR.map(i => Math.ceil((gridWidth / width) * i)), 28 | ), 29 | AR.map(i => row[i]), 30 | init, 31 | tail, 32 | addAround([[begin], [end]]), 33 | ); 34 | }; 35 | 36 | const shrinkRows: Unary> = width => cells => 37 | cells.length === 0 ? [] : FN.pipe(cells, FN.pipe(width, shrinkRow, AR.map)); 38 | 39 | /** 40 | * Shrink rows of cells to given size 41 | * 42 | * Shrinking a row by N cells will remove one cell every width/N cells. 43 | * Shrinking the row `1234` to `width=2`, for example, will return the row `13`. 44 | * 45 | */ 46 | export const shrinkCells: Unary> = ({ width, height }) => 47 | FN.flow(shrinkRows(width), transpose, shrinkRows(height), transpose); 48 | 49 | /** The opposite of `stretch` */ 50 | export const shrink: Unary> = size => 51 | FN.flow(TY.unpack, shrinkCells(size), TY.pack); 52 | -------------------------------------------------------------------------------- /src/grid/tests/crop.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as IUT from '../crop'; 3 | import * as TY from '../types'; 4 | 5 | suite('grid shrink', () => { 6 | test('1 top 1 left', () => 7 | assert.deepEqual( 8 | TY.size( 9 | IUT.crop({ top: 1, right: 0, bottom: 0, left: 1 })( 10 | TY.sized({ width: 3, height: 5 }), 11 | ), 12 | ), 13 | { width: 2, height: 4 }, 14 | )); 15 | }); 16 | -------------------------------------------------------------------------------- /src/grid/tests/expand.test.ts: -------------------------------------------------------------------------------- 1 | import { function as FN, array as AR } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import * as IUT from '../expand'; 4 | import * as TY from '../types'; 5 | import * as CE from 'src/cell'; 6 | import { get } from '../ops'; 7 | 8 | suite('grid expand', () => { 9 | test('empty + 1', () => 10 | assert.deepEqual( 11 | TY.size(IUT.expand({ top: 1, right: 1, bottom: 0, left: 0 })(TY.empty())), 12 | { width: 1, height: 1 }, 13 | )); 14 | 15 | test('align center/middle', () => 16 | assert.deepEqual( 17 | FN.pipe( 18 | 'x', 19 | CE.plainChar, 20 | AR.of, 21 | AR.of, 22 | TY.pack, 23 | IUT.expand({ top: 1, right: 1, bottom: 1, left: 1 }), 24 | get([1, 1]), 25 | CE.rune.get, 26 | ), 27 | 'x', 28 | )); 29 | }); 30 | -------------------------------------------------------------------------------- /src/grid/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { array as AR, function as FN } from 'fp-ts'; 3 | import * as CE from 'src/cell'; 4 | import { Cell } from 'src/cell'; 5 | import { narrowOrNoneArb } from 'src/cell/tests/helpers'; 6 | import { Size, size as SZ } from 'src/geometry'; 7 | import { head } from 'util/array'; 8 | import { Tuple4 } from 'util/tuple'; 9 | import { assert, test } from 'vitest'; 10 | import * as PA from '../paint'; 11 | import * as TY from '../types'; 12 | import { Grid, unpack } from '../types'; 13 | 14 | export * from 'src/cell/tests/helpers'; 15 | 16 | export type CellArb = fc.Arbitrary; 17 | 18 | export const gridEq = (fst: Grid, snd: Grid): void => 19 | assert.deepEqual(unpack(fst), unpack(snd)); 20 | 21 | export const plainGrid = FN.flow( 22 | FN.pipe(CE.plainChar, AR.map, AR.map), 23 | TY.pack, 24 | ); 25 | 26 | export const narrowRedCell = FN.pipe('x', CE.fgChar(0xff_00_00_ff), head), 27 | narrowRed1x1 = TY.oneCell(narrowRedCell); 28 | 29 | export const sizeArb: fc.Arbitrary = fc.record({ 30 | width: fc.nat(6), 31 | height: fc.nat(6), 32 | }); 33 | 34 | // given a number n returns array len n gen of non or narrow cells 35 | const narrowOrNoneTuple = (n: N) => 36 | fc.tuple( 37 | // getting around the 21 overloads of fc.tuple 38 | ...(AR.replicate(n, narrowOrNoneArb) as unknown as Tuple4), 39 | ) as unknown as fc.Arbitrary; 40 | 41 | export const narrowGridArb: fc.Arbitrary = sizeArb.chain(size => 42 | FN.pipe(size, SZ.area, narrowOrNoneTuple) 43 | .map(AR.chunksOf(size.width)) 44 | .map(TY.pack), 45 | ); 46 | 47 | export const paint = PA.paintWith('.'); 48 | 49 | export const testPaint = (name: string, actual: Grid, expect: string[]) => 50 | test(name, () => assert.deepEqual(PA.paintWith('.')(actual), expect)); 51 | -------------------------------------------------------------------------------- /src/grid/tests/instances.test.ts: -------------------------------------------------------------------------------- 1 | import * as laws from 'fp-ts-laws'; 2 | import { align as AL } from 'src/align'; 3 | import { assert, suite, test } from 'vitest'; 4 | import * as IUT from '../instances'; 5 | import { resize } from '../resize'; 6 | import { narrowRed1x1, narrowGridArb } from './helpers'; 7 | 8 | suite('grid instances', () => { 9 | test('show', () => { 10 | const ninth = resize(AL.middleCenter)({ width: 3, height: 3 })( 11 | narrowRed1x1, 12 | ); 13 | assert.equal(IUT.show.show(ninth), '3ˣ3 11% non-empty'); 14 | }); 15 | 16 | suite('laws', () => { 17 | test('eq', () => laws.eq(IUT.eq, narrowGridArb)); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/grid/tests/paint.test.ts: -------------------------------------------------------------------------------- 1 | import ansis from 'ansis'; 2 | import * as color from 'src/color'; 3 | import { assert, suite, test } from 'vitest'; 4 | import { paint } from '../paint'; 5 | import { parseRows } from '../parse'; 6 | import { gridEq } from './helpers'; 7 | 8 | suite('grid paint', () => { 9 | const testPaint = (name: string, rows: string[]) => { 10 | const expect = parseRows('left', rows), 11 | actual = parseRows('left', paint(expect)); 12 | 13 | test(name, () => gridEq(actual, expect)); 14 | }; 15 | 16 | testPaint('empty', []); 17 | 18 | testPaint('narrow plain 1x1', ['a']); 19 | 20 | testPaint('wide', ['🙂']); 21 | 22 | testPaint('narrow plain 3x2', ['abc', '123']); 23 | 24 | testPaint('red narrow', [ansis.red('red')]); 25 | 26 | testPaint('bold red narrow', [ansis.red.bold('x')]); 27 | 28 | testPaint('fg+bg', [ansis.red.bgBlue('red-on-blue')]); 29 | 30 | test('alternating fg+bg changes', () => { 31 | const fgRedBgBlue = color.of(['red', 'blue']), 32 | fgRedBgGreen = color.of(['red', 'green']), 33 | fgWhiteBgGreen = color.of(['white', 'green']); 34 | 35 | const expect = [fgRedBgBlue('a') + fgRedBgGreen('b') + fgWhiteBgGreen('c')]; 36 | 37 | const actual = paint(parseRows('left', expect)); 38 | 39 | assert.deepEqual(actual, expect); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/grid/tests/resize.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as IUT from '../resize'; 3 | import * as TY from '../types'; 4 | import { align } from 'src/align'; 5 | 6 | suite('grid resize', () => { 7 | test('-w +h', () => 8 | assert.deepEqual( 9 | TY.size( 10 | IUT.resize(align.middleCenter)({ width: 4, height: 2 })( 11 | TY.sized({ width: 3, height: 5 }), 12 | ), 13 | ), 14 | { width: 4, height: 2 }, 15 | )); 16 | }); 17 | -------------------------------------------------------------------------------- /src/grid/tests/shrink.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import * as CE from 'src/cell'; 4 | import { shrink } from '../shrink'; 5 | import * as TY from '../types'; 6 | import { gridEq } from './helpers'; 7 | 8 | const makeGrid = FN.flow(FN.pipe(CE.plainChar, AR.map, AR.map), TY.pack); 9 | 10 | suite('grid shrink', () => { 11 | suite('6x4 ⇒ 3x2', () => { 12 | const source = makeGrid([ 13 | ['a', 'b', 'c', 'd', 'e', 'f'], 14 | ['g', 'h', 'i', 'j', 'k', 'l'], 15 | ]); 16 | 17 | const actual = FN.pipe(source, shrink({ width: 3, height: 2 })); 18 | 19 | test('size', () => { 20 | assert.deepEqual(TY.size(actual), { 21 | width: 3, 22 | height: 2, 23 | }); 24 | }); 25 | 26 | const expect = makeGrid([ 27 | ['a', 'c', 'f'], 28 | ['g', 'i', 'l'], 29 | ]); 30 | 31 | test('content', () => gridEq(actual, expect)); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/grid/tests/stack.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'vitest'; 2 | import * as PA from '../parse'; 3 | import * as IUT from '../stack'; 4 | import { testPaint } from './helpers'; 5 | 6 | suite('grid stack', () => { 7 | const narrowRow = '..xx'.replaceAll('.', ' '); 8 | testPaint( 9 | 'narrow above wide', 10 | IUT.stack('over')([PA.parseRow('🙂😢'), PA.parseRow(narrowRow)]), 11 | ['🙂xx'], 12 | ); 13 | 14 | testPaint( 15 | 'narrow above wide 2 rows', 16 | IUT.stack('over')([ 17 | PA.parseRows('left', ['🙂😢', '🙂😢']), 18 | PA.parseRows('left', [narrowRow, narrowRow]), 19 | ]), 20 | ['🙂xx', '🙂xx'], 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /src/grid/tests/stretch.test.ts: -------------------------------------------------------------------------------- 1 | import { array as AR, function as FN } from 'fp-ts'; 2 | import { assert, suite, test } from 'vitest'; 3 | import * as CE from 'src/cell'; 4 | import { stretch } from '../stretch'; 5 | import * as TY from '../types'; 6 | import { gridEq } from './helpers'; 7 | 8 | suite('grid stretch', () => { 9 | test('empty + 1', () => 10 | assert.deepEqual(TY.size(stretch({ width: 1, height: 1 })(TY.empty())), { 11 | width: 1, 12 | height: 1, 13 | })); 14 | 15 | suite('oneCell + 2x3', () => { 16 | const cell = CE.plainChar('x'); 17 | 18 | const actual = FN.pipe(cell, TY.oneCell, stretch({ width: 2, height: 3 })); 19 | 20 | test('size', () => { 21 | assert.deepEqual(TY.size(actual), { 22 | width: 2, 23 | height: 3, 24 | }); 25 | }); 26 | 27 | const expect = TY.pack(AR.replicate(3, [cell, cell])); 28 | 29 | test('content', () => gridEq(actual, expect)); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/grid/tests/types.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, suite, test } from 'vitest'; 2 | import * as CE from 'src/cell'; 3 | import { Cell } from 'src/cell'; 4 | import * as TY from '../types'; 5 | 6 | suite('grid types', () => { 7 | test('empty row count', () => assert.equal(TY.countRows(TY.empty()), 0)); 8 | 9 | test('size', () => 10 | assert.deepEqual(TY.size(TY.sized({ width: 3, height: 4 })), { 11 | width: 3, 12 | height: 4, 13 | })); 14 | 15 | suite('pack/unpack', () => { 16 | const testRoundTrip = (name: string, cells: Cell[][]) => 17 | test(name, () => assert.deepEqual(TY.unpack(TY.pack(cells)), cells)); 18 | 19 | testRoundTrip('one empty cell', [[CE.none]]); 20 | 21 | testRoundTrip('one narrow red cell', [CE.fgChar(0xff_00_00_ff)('x')]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/report.ts: -------------------------------------------------------------------------------- 1 | export * from './report/relation'; 2 | export * from './report/chain'; 3 | -------------------------------------------------------------------------------- /src/stacka.ts: -------------------------------------------------------------------------------- 1 | export * as backdrop from './backdrop'; 2 | export * as cell from './cell'; 3 | export * as color from './color'; 4 | export * as grid from './grid'; 5 | export * as style from './style'; 6 | export * from './align'; 7 | export * from './bitmap'; 8 | export * from './block'; 9 | export * from './border'; 10 | export * from './box'; 11 | export * from './boxes'; 12 | export * from './geometry'; 13 | export * from './glyph'; 14 | export * from './report'; 15 | export * from './term'; 16 | 17 | export type { Style, Deco, DecoList } from 'src/style'; 18 | export type { Cell } from 'src/cell'; 19 | export type { Level, Color, ColorName, BlendMode } from 'src/color'; 20 | export type { Grid } from 'src/grid'; 21 | export type { Backdrop } from 'src/backdrop'; 22 | -------------------------------------------------------------------------------- /src/style.ts: -------------------------------------------------------------------------------- 1 | export * from './style/style'; 2 | -------------------------------------------------------------------------------- /src/style/blend.ts: -------------------------------------------------------------------------------- 1 | import { function as FN } from 'fp-ts'; 2 | import { mapBoth } from 'fp-ts-std/Tuple'; 3 | import * as color from 'src/color'; 4 | import { BlendMode } from 'src/color'; 5 | import { BinOpT, Unary } from 'util/function'; 6 | import * as DE from './deco'; 7 | import * as LE from './lens'; 8 | import * as TY from './types'; 9 | import { Style } from './types'; 10 | 11 | export const blend: Unary> = 12 | mode => 13 | ([lower, upper]) => { 14 | if (TY.isEmpty(upper)) return lower; 15 | else if (TY.isEmpty(lower)) return upper; 16 | else if (mode === 'under' || mode === 'combineUnder') return lower; 17 | else if (mode === 'over' || mode === 'combineOver') return upper; 18 | 19 | const [[lowerFg, lowerBg], [upperFg, upperBg]] = FN.pipe( 20 | [lower, upper], 21 | mapBoth(TY.asColorPair), 22 | ), 23 | [lowerDeco, upperDeco] = FN.pipe([lower, upper], mapBoth(LE.deco.get)), 24 | blendColor = color.blend(mode); 25 | 26 | const fgRes = blendColor([lowerFg, upperFg]), 27 | bgRes = blendColor([lowerBg, upperBg]), 28 | decoRes = DE.monoid.concat(lowerDeco, upperDeco); 29 | 30 | const res: Style = [ 31 | color.normalize(fgRes), 32 | color.normalize(bgRes), 33 | decoRes, 34 | ]; 35 | 36 | return res; 37 | }; 38 | -------------------------------------------------------------------------------- /src/style/instances.ts: -------------------------------------------------------------------------------- 1 | import { eq as EQ, function as FN, monoid as MO, show as SH } from 'fp-ts'; 2 | import { mapBoth } from 'fp-ts-std/Tuple'; 3 | import * as color from 'src/color'; 4 | import { BlendMode } from 'src/color'; 5 | import { Unary } from 'util/function'; 6 | import { Pair } from 'util/tuple'; 7 | import { blend } from './blend'; 8 | import { bgLens, deco, decoList, fgLens } from './lens'; 9 | import * as TY from './types'; 10 | import { Style } from './types'; 11 | 12 | export const eq: EQ.Eq