├── .gitignore ├── .vscode └── settings.json ├── test.ts ├── forgit ├── deno.jpg ├── img01.png ├── img02.png └── deno50.jpg ├── models ├── IStyleBase.ts ├── ColorStyle.ts ├── BackgroundColorStyle.ts ├── StyleBase.ts └── FontStyle.ts ├── test_deps.ts ├── .travis.yml ├── egg.json ├── .circleci └── config.yml ├── .github └── workflows │ └── main.yml ├── mod_test.ts ├── LICENSE ├── htmlToAnsi.ts ├── utils_test.ts ├── imageToAnsi.ts ├── tsdom ├── styleparser.ts └── tsdom.ts ├── mod.ts ├── utils.ts ├── tslint.json ├── README.md └── welcome.ts /.gitignore: -------------------------------------------------------------------------------- 1 | local_test.ts 2 | temp -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deepscan.enable": true 3 | } -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import "./mod_test.ts" 2 | import "./utils_test.ts" 3 | -------------------------------------------------------------------------------- /forgit/deno.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakoua/ink/HEAD/forgit/deno.jpg -------------------------------------------------------------------------------- /forgit/img01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakoua/ink/HEAD/forgit/img01.png -------------------------------------------------------------------------------- /forgit/img02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakoua/ink/HEAD/forgit/img02.png -------------------------------------------------------------------------------- /forgit/deno50.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakoua/ink/HEAD/forgit/deno50.jpg -------------------------------------------------------------------------------- /models/IStyleBase.ts: -------------------------------------------------------------------------------- 1 | export interface IStyleBase { 2 | name: string 3 | rules: string 4 | ansiStart(): string 5 | ansiEnd(): string 6 | } 7 | -------------------------------------------------------------------------------- /test_deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | assert, 3 | assertEquals, 4 | assertThrows, 5 | assertThrowsAsync 6 | } from "https://deno.land/std/testing/asserts.ts"; 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | install: 4 | - curl -L https://deno.land/x/install/install.sh | sh 5 | - export PATH="$HOME/.deno/bin:$PATH" 6 | 7 | script: 8 | - deno test -A 9 | -------------------------------------------------------------------------------- /egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ink", 3 | "description": "Terminal string color for deno.", 4 | "stable": true, 5 | "files": [ 6 | "./htmlToAnsi.ts", 7 | "./imageToAnsi.ts", 8 | "./LICENSE.ts", 9 | "./mod.ts", 10 | "./mod_test.ts", 11 | "./README.md", 12 | "./test.ts", 13 | "./test_deps.ts", 14 | "./utils.ts", 15 | "./utils_test.ts", 16 | "./welcome.ts", 17 | "./forgit/**/*", 18 | "./models/**/*", 19 | "./tsdom/**/*" 20 | ] 21 | } -------------------------------------------------------------------------------- /models/ColorStyle.ts: -------------------------------------------------------------------------------- 1 | import { StyleBase } from "./StyleBase.ts"; 2 | import { rgbToAnsi, hexToAnsi } from "../utils.ts"; 3 | 4 | export class ColorStyle extends StyleBase { 5 | 6 | constructor(rules: string) { 7 | super("color", rules) 8 | } 9 | 10 | ansiStart(): string { 11 | const rgb = this.rules.indexOf("rgb") >= 0 ? rgbToAnsi(this.rules) : hexToAnsi(this.rules); 12 | return `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` 13 | } 14 | ansiEnd(): string { 15 | return "\x1b[39m"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /models/BackgroundColorStyle.ts: -------------------------------------------------------------------------------- 1 | import { StyleBase } from "./StyleBase.ts"; 2 | import { rgbToAnsi, hexToAnsi } from "../utils.ts"; 3 | 4 | export class BackgroundColorStyle extends StyleBase { 5 | 6 | constructor(rules: string) { 7 | super("background-color", rules) 8 | } 9 | 10 | ansiStart(): string { 11 | const rgb = this.rules.indexOf("rgb") >= 0 ? rgbToAnsi(this.rules) : hexToAnsi(this.rules); 12 | return `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` 13 | } 14 | ansiEnd(): string { 15 | return "\x1b[49m"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /models/StyleBase.ts: -------------------------------------------------------------------------------- 1 | import type { IStyleBase } from "./IStyleBase.ts"; 2 | import { normalizeColorString } from "../utils.ts"; 3 | 4 | export class StyleBase implements IStyleBase { 5 | name: string; 6 | rules: string; 7 | 8 | constructor(name: string, rules: string) { 9 | this.name = name; 10 | this.rules = normalizeColorString(rules); 11 | } 12 | 13 | ansiStart(): string { 14 | throw new Error("Method not implemented."); 15 | } 16 | ansiEnd(): string { 17 | throw new Error("Method not implemented."); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: maxmcd/deno 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | - run: deno test -A 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deno CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | name: ${{ matrix.kind }} ${{ matrix.os }} 9 | runs-on: ${{ matrix.os }} 10 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 11 | strategy: 12 | matrix: 13 | os: [macOS-latest, ubuntu-latest, windows-latest] 14 | env: 15 | GH_ACTIONS: true 16 | DENO_BUILD_MODE: release 17 | V8_BINARY: true 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup Deno 21 | uses: denolib/setup-deno@master 22 | with: 23 | deno-version: 1.x 24 | - name: Tests 25 | run: deno test -A --unstable 26 | -------------------------------------------------------------------------------- /mod_test.ts: -------------------------------------------------------------------------------- 1 | import * as ink from "./mod.ts" 2 | import "./mod.ts" 3 | 4 | import { assertEquals } from "./test_deps.ts" 5 | 6 | Deno.test("test_output", function () { 7 | ink.list() 8 | const text = ink.colorize("hello") 9 | assertEquals(text, "\x1b[31mhello\x1b[39m") 10 | }); 11 | 12 | Deno.test("test_extension", function () { 13 | assertEquals("deno".toColor(), "deno") 14 | }) 15 | 16 | Deno.test("test_drawImage", function () { 17 | // able to draw an image 18 | // await ink.drawImage("https://placekitten.com/10/10") 19 | assertEquals(1, 1); 20 | }) 21 | 22 | Deno.test("test_terminal", async function () { 23 | // able to draw an image 24 | await ink.terminal.log("h"); 25 | assertEquals(1, 1); 26 | }) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sameh Fakoua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /htmlToAnsi.ts: -------------------------------------------------------------------------------- 1 | import { parse, HTMLElement, Node, TextNode } from "./tsdom/tsdom.ts" 2 | import { StyleParser } from "./tsdom/styleparser.ts" 3 | 4 | export function toAnsi(html: string): string { 5 | let data = `${html}` 6 | data = data.replace(/\/gi, "
") 7 | data = data.replace(/\/gi, "
") 8 | data = data.trim() 9 | const root = parse(data) 10 | if (root instanceof HTMLElement) { 11 | const res = getInked(root.firstChild); 12 | return res; 13 | } 14 | return html; // in case of error 15 | } 16 | 17 | function getInked(n: Node, parentAnsiStart: string = ""): string { 18 | let rtnVal = "" 19 | n.childNodes.forEach(el => { 20 | if (el instanceof TextNode) { 21 | rtnVal += el.text 22 | } 23 | if (el instanceof HTMLElement) { 24 | const style = el.attributes.style 25 | StyleParser.parse(style); 26 | const ansiStart = StyleParser.toAnsiStart() 27 | const ansiEnd = `${StyleParser.toAnsiEnd()}${parentAnsiStart}` 28 | 29 | rtnVal += `${ansiStart}${getInked(el, ansiStart)}${ansiEnd}` 30 | } 31 | }) 32 | return rtnVal 33 | } 34 | -------------------------------------------------------------------------------- /utils_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "./test_deps.ts" 2 | import * as utils from "./utils.ts" 3 | 4 | Deno.test("test_utils_hexToAnsi", function () { 5 | assertEquals(utils.hexToAnsi("#ff0000"), {r: 255, g: 0, b: 0}) 6 | assertEquals(utils.hexToAnsi("ff0000"), {r: 255, g: 0, b: 0}) 7 | assertEquals(utils.hexToAnsi(" #ff0000"), {r: 255, g: 0, b: 0}) 8 | assertEquals(utils.hexToAnsi(" ff0000 "), {r: 255, g: 0, b: 0}) 9 | assertEquals(utils.hexToAnsi("#ffffff"), {r: 255, g: 255, b: 255}) 10 | assertEquals(utils.hexToAnsi("#32640c"), {r: 50, g: 100, b: 12}) 11 | assertEquals(utils.hexToAnsi("#fFFffF"), {r: 255, g: 255, b: 255}) 12 | }) 13 | 14 | Deno.test("test_utils_rgbToAnsi", function () { 15 | assertEquals(utils.rgbToAnsi("rgb(1,1,1)"), {r: 1, g: 1, b: 1}) 16 | assertEquals(utils.rgbToAnsi(" rgb(1,1,1)"), {r: 1, g: 1, b: 1}) 17 | assertEquals(utils.rgbToAnsi(" rgb(1,1,1) "), {r: 1, g: 1, b: 1}) 18 | assertEquals(utils.rgbToAnsi(" rgb( 1 ,1 , 1 ) "), {r: 1, g: 1, b: 1}) 19 | assertEquals(utils.rgbToAnsi("RGB(1,1,1)"), {r: 1, g: 1, b: 1}) 20 | assertEquals(utils.rgbToAnsi("rgb(10,255,0)"), {r: 10, g: 255, b: 0}) 21 | }) 22 | 23 | Deno.test("test_utils_rgbLoop", function () { 24 | for (let r = 0; r < 256; r++) { 25 | const rgbCalculated = utils.rgbToAnsi(`rgb(${r}, 0, 0)`) 26 | const rgbReal = {r: r, g: 0, b: 0} 27 | assertEquals(rgbCalculated, rgbReal) 28 | } 29 | }) 30 | 31 | Deno.test("test_util_normalizeColorString", function () { 32 | assertEquals(utils.normalizeColorString(" hello "), "hello") 33 | assertEquals(utils.normalizeColorString(" #hello "), "hello") 34 | assertEquals(utils.normalizeColorString(" #he llo "), "hello") 35 | assertEquals(utils.normalizeColorString(" # hEllO "), "hello") 36 | }) 37 | -------------------------------------------------------------------------------- /models/FontStyle.ts: -------------------------------------------------------------------------------- 1 | import { StyleBase } from "./StyleBase.ts"; 2 | 3 | interface FontStyleType { 4 | mode: string; 5 | ansiStartCode: number; 6 | ansiEndCode: number; 7 | } 8 | 9 | export class FontStyle extends StyleBase { 10 | private fonts: Array = [ 11 | { 12 | mode: "bold", 13 | ansiStartCode: 1, 14 | ansiEndCode: 22, 15 | }, 16 | { 17 | mode: "dim", 18 | ansiStartCode: 2, 19 | ansiEndCode: 22, 20 | }, 21 | { 22 | mode: "italic", 23 | ansiStartCode: 3, 24 | ansiEndCode: 23, 25 | }, 26 | { 27 | mode: "underline", 28 | ansiStartCode: 4, 29 | ansiEndCode: 24, 30 | }, 31 | { 32 | mode: "inverse", 33 | ansiStartCode: 7, 34 | ansiEndCode: 27, 35 | }, 36 | { 37 | mode: "hidden", 38 | ansiStartCode: 8, 39 | ansiEndCode: 28, 40 | }, 41 | { 42 | mode: "strikethrough", 43 | ansiStartCode: 9, 44 | ansiEndCode: 29, 45 | }, 46 | ]; 47 | 48 | private fontModes: Array; 49 | 50 | private modeToAnsi(mode: string): { start: string; end: string } { 51 | mode = mode.trim().toLowerCase(); 52 | const f = this.fonts.find((ff) => { 53 | return ff.mode === mode; 54 | }); 55 | if (f === undefined) { 56 | return { 57 | start: "", 58 | end: "", 59 | }; 60 | } else { 61 | return { 62 | start: `\x1b[${f.ansiStartCode}m`, 63 | end: `\x1b[${f.ansiEndCode}m`, 64 | }; 65 | } 66 | } 67 | 68 | constructor(rules: string) { 69 | super("font", rules); 70 | this.fontModes = rules.split(","); 71 | } 72 | 73 | ansiStart(): string { 74 | let rntVal = ""; 75 | this.fontModes.map((m) => { 76 | rntVal += this.modeToAnsi(m).start; 77 | }); 78 | return rntVal; 79 | } 80 | ansiEnd(): string { 81 | let rntVal = ""; 82 | this.fontModes.map((m) => { 83 | rntVal += this.modeToAnsi(m).end; 84 | }); 85 | return rntVal; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /imageToAnsi.ts: -------------------------------------------------------------------------------- 1 | import { decode, Pixel, Image } from "https://deno.land/x/jpegts/mod.ts" 2 | 3 | /** 4 | * draw an image to the terminal. 5 | * @param imagePath local path or URL 6 | */ 7 | export async function drawImage(imagePath: string) { 8 | let raw: Uint8Array 9 | 10 | if (isUrl(imagePath)) { 11 | const res = await fetch(imagePath) 12 | const blob = await res.blob(); 13 | raw = new Uint8Array(await blob.arrayBuffer()) 14 | 15 | } else { 16 | raw = await Deno.readFile(imagePath) 17 | } 18 | 19 | 20 | const image = decode(raw); 21 | const rImage = resizeImage(image, image.width, Math.ceil(image.height / 2)) 22 | let appender = ""; 23 | for (let y = 0; y < rImage.height; y++) { 24 | for (let x = 0; x < rImage.width; x++) { 25 | const pix = rImage.getPixel(x, y); 26 | appender += pixelToAnsi(pix) 27 | } 28 | appender += "\r\n" + "\x1b[0m" 29 | } 30 | console.log(appender) 31 | } 32 | 33 | function isUrl(path: string): boolean { 34 | return path.toLowerCase().startsWith("http://") || path.toLowerCase().startsWith("https://") 35 | } 36 | 37 | function pixelToAnsi(pix: Pixel) { 38 | const bgStart = `\x1b[48;2;${pix.r};${pix.g};${pix.b}m` 39 | 40 | const cStart = `\x1b[38;2;${pix.r};${pix.g};${pix.b}m` 41 | 42 | return `${bgStart}${cStart}▀` 43 | // return `${cStart}.${cEnd}` 44 | } 45 | 46 | 47 | function resizeImage(image: Image, w2: number, h2: number): Image { 48 | const result = new Image(); 49 | result.width = w2 50 | result.height = h2 51 | result.data = new Uint8Array(w2 * h2 * 4); 52 | 53 | const x_ratio = image.width / w2; 54 | const y_ratio = image.height / h2; 55 | let px: number, py: number, pix: Pixel 56 | for (let i = 0; i < h2; i++) { 57 | for (let j = 0; j < w2; j++) { 58 | px = Math.floor(j * x_ratio) 59 | py = Math.floor(i * y_ratio) 60 | pix = image.getPixel(px, py) 61 | // set pixel 62 | result.setPixel(j, i, pix) 63 | } 64 | } 65 | 66 | return result 67 | } 68 | -------------------------------------------------------------------------------- /tsdom/styleparser.ts: -------------------------------------------------------------------------------- 1 | import { ColorStyle } from "../models/ColorStyle.ts" 2 | import { BackgroundColorStyle } from "../models/BackgroundColorStyle.ts" 3 | import { FontStyle } from "../models/FontStyle.ts" 4 | 5 | export abstract class StyleParser { 6 | 7 | static color: ColorStyle 8 | static backgroundColor: BackgroundColorStyle 9 | static font: FontStyle 10 | 11 | static toAnsiStart(): string { 12 | let rtnVal = "" 13 | 14 | if (this.color) { 15 | rtnVal += this.color.ansiStart(); 16 | } 17 | 18 | if (this.backgroundColor) { 19 | rtnVal += this.backgroundColor.ansiStart(); 20 | } 21 | 22 | if (this.font) { 23 | rtnVal += this.font.ansiStart(); 24 | } 25 | 26 | return rtnVal; 27 | } 28 | 29 | static toAnsiEnd(): string { 30 | let rtnVal = "" 31 | 32 | if (this.color) { 33 | rtnVal += this.color.ansiEnd(); 34 | } 35 | 36 | if (this.backgroundColor) { 37 | rtnVal += this.backgroundColor.ansiEnd(); 38 | } 39 | 40 | if (this.font) { 41 | rtnVal += this.font.ansiEnd(); 42 | } 43 | 44 | return rtnVal; 45 | } 46 | 47 | static parse(style: string): void { 48 | const styles = style.split(";") 49 | styles.forEach(element => { 50 | this.parseSubStyle(element) 51 | }); 52 | } 53 | 54 | private static parseSubStyle(subStyle: string) { 55 | subStyle = subStyle.trim(); 56 | if (subStyle !== "") { 57 | const st = subStyle.split(":") 58 | const name = st[0].trim(); 59 | const value = st[1].trim(); 60 | 61 | switch (name.toLowerCase()) { 62 | case "color": 63 | this.color = new ColorStyle(value); 64 | break; 65 | case "background-color": 66 | this.backgroundColor = new BackgroundColorStyle(value) 67 | break; 68 | case "font": 69 | this.font = new FontStyle(value) 70 | break; 71 | default: 72 | break; 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { replacer, tags } from "./utils.ts" 2 | import { toAnsi } from "./htmlToAnsi.ts" 3 | export { drawImage } from "./imageToAnsi.ts" 4 | 5 | declare global { 6 | interface String { 7 | toColor(): string 8 | } 9 | } 10 | 11 | String.prototype.toColor = function (): string { 12 | return html(this as string); 13 | } 14 | 15 | // tslint:disable-next-line 16 | export class terminal { 17 | // @ts-ignore 18 | static log(...args): void { 19 | args = processArgs(args) 20 | console.log(...args) 21 | } 22 | // @ts-ignore 23 | static trace(...args): void { 24 | args = processArgs(args) 25 | console.trace(...args) 26 | } 27 | // @ts-ignore 28 | static warn(...args): void { 29 | args = processArgs(args) 30 | console.warn(...args) 31 | } 32 | // @ts-ignore 33 | static error(...args): void { 34 | args = processArgs(args) 35 | console.error(...args) 36 | } 37 | // @ts-ignore 38 | static debug(...args): void { 39 | args = processArgs(args) 40 | console.debug(...args) 41 | } 42 | } 43 | 44 | export const colorize = function (input: string): string { 45 | return replacer(input) 46 | } 47 | 48 | export const html = function (htm: string): string { 49 | return toAnsi(htm); 50 | } 51 | 52 | export const list = function (): void { 53 | Object.keys(tags).forEach(key => { 54 | if (key.indexOf("/") < 0) { 55 | const tagName = key.replace("<", "").replace(">", "") 56 | const tagStart = `<${tagName}>` 57 | const tagEnd = `` 58 | let text = `-->${tagStart}tag: ${tagName}${tagEnd}` 59 | text = replacer(text) 60 | console.log(text) 61 | } 62 | }) 63 | } 64 | 65 | // Extension 66 | 67 | // @ts-ignore 68 | function processArgs(args) { 69 | // @ts-ignore 70 | return args.map((value) => { 71 | if (typeof value === "string") { 72 | return replacer(value) 73 | } else { 74 | return value 75 | } 76 | }) 77 | } 78 | 79 | // CLI 80 | if (Deno.args.length === 1) { 81 | let cmd = Deno.args[0] 82 | cmd = cmd.replace("--", "").replace("-", "") 83 | if (cmd === "list") { 84 | list() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | export const rgbToAnsi = function (rgb: string): { r: number, g: number, b: number } { 2 | rgb = normalizeColorString(rgb) 3 | const result = /^rgb\((\d+)\,(\d+)\,(\d+)\)$/i.exec(rgb); 4 | return { 5 | // @ts-ignore 6 | r: parseInt(result[1], 10), 7 | // @ts-ignore 8 | g: parseInt(result[2], 10), 9 | // @ts-ignore 10 | b: parseInt(result[3], 10), 11 | } 12 | } 13 | 14 | export const hexToAnsi = function (hex: string): { r: number, g: number, b: number } { 15 | hex = normalizeColorString(hex) 16 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 17 | return { 18 | // @ts-ignore 19 | r: parseInt(result[1], 16), 20 | // @ts-ignore 21 | g: parseInt(result[2], 16), 22 | // @ts-ignore 23 | b: parseInt(result[3], 16), 24 | } 25 | } 26 | 27 | export const normalizeColorString = function (input: string): string { 28 | input = input.replace("#", ""); 29 | input = input.replace(/\s/g, ""); 30 | input = input.trim(); 31 | input = input.toLowerCase(); 32 | return input; 33 | } 34 | 35 | export const tags = { 36 | "": 1, 37 | "": 22, 38 | "": 2, 39 | "": 22, 40 | "": 3, 41 | "": 23, 42 | "": 4, 43 | "": 24, 44 | "": 7, 45 | "": 27, 46 | "": 8, 47 | "": 28, 48 | "": 9, 49 | "": 29, 50 | "": 30, 51 | "": 39, 52 | "": 31, 53 | "": 39, 54 | "": 32, 55 | "": 39, 56 | "": 33, 57 | "": 39, 58 | "": 34, 59 | "": 39, 60 | "": 35, 61 | "": 39, 62 | "": 36, 63 | "": 39, 64 | "": 37, 65 | "": 39, 66 | "": 90, 67 | "": 39, 68 | "": 40, 69 | "": 49, 70 | "": 41, 71 | "": 49, 72 | "": 42, 73 | "": 49, 74 | "": 43, 75 | "": 49, 76 | "": 44, 77 | "": 49, 78 | "": 45, 79 | "": 49, 80 | "": 40, 81 | "": 46, 82 | "": 47, 83 | "": 49, 84 | } 85 | 86 | export function replacer(input: string): string { 87 | let output = input 88 | Object.keys(tags).forEach(key => { 89 | const re = new RegExp(key, "gi") 90 | // @ts-ignore 91 | output = output.replace(re, `\x1b[${tags[key]}m`) 92 | }) 93 | output = output.replace(/\<\;/g, "<") 94 | return output 95 | } 96 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ ], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "import-blacklist": [ 15 | true, 16 | "rxjs" 17 | ], 18 | "import-spacing": true, 19 | "indent": [ 20 | true, 21 | "spaces" 22 | ], 23 | "interface-over-type-literal": true, 24 | "label-position": true, 25 | "max-line-length": [ 26 | true, 27 | 140 28 | ], 29 | "member-access": false, 30 | "no-arg": true, 31 | "no-bitwise": true, 32 | "no-console": [ 33 | false, 34 | "debug", 35 | "info", 36 | "time", 37 | "timeEnd", 38 | "trace" 39 | ], 40 | "no-construct": true, 41 | "no-debugger": true, 42 | "no-duplicate-super": true, 43 | "no-empty": false, 44 | "no-empty-interface": true, 45 | "no-eval": true, 46 | "no-inferrable-types": [ 47 | true, 48 | "ignore-params" 49 | ], 50 | "no-misused-new": true, 51 | "no-non-null-assertion": true, 52 | "no-shadowed-variable": true, 53 | "no-string-literal": false, 54 | "no-string-throw": true, 55 | "no-switch-case-fall-through": true, 56 | "no-trailing-whitespace": false, 57 | "no-unnecessary-initializer": true, 58 | "no-unused-expression": true, 59 | "no-var-keyword": true, 60 | "object-literal-sort-keys": false, 61 | "one-line": [ 62 | true, 63 | "check-open-brace", 64 | "check-catch", 65 | "check-else", 66 | "check-whitespace" 67 | ], 68 | "prefer-const": true, 69 | "quotemark": [ 70 | true, 71 | "double" 72 | ], 73 | "radix": true, 74 | "semicolon": [ 75 | "always" 76 | ], 77 | "triple-equals": [ 78 | true, 79 | "allow-null-check" 80 | ], 81 | "typedef-whitespace": [ 82 | true, 83 | { 84 | "call-signature": "nospace", 85 | "index-signature": "nospace", 86 | "parameter": "nospace", 87 | "property-declaration": "nospace", 88 | "variable-declaration": "nospace" 89 | } 90 | ], 91 | "unified-signatures": true, 92 | "variable-name": false, 93 | "whitespace": [ 94 | true, 95 | "check-branch", 96 | "check-decl", 97 | "check-operator", 98 | "check-separator", 99 | "check-type" 100 | ] 101 | } 102 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ink 2 | 3 | Terminal string color for deno 4 | 5 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/fakoua/ink?style=for-the-badge) 6 | ![GitHub](https://img.shields.io/github/license/fakoua/ink?style=for-the-badge) 7 | ![GitHub last commit](https://img.shields.io/github/last-commit/fakoua/ink?style=for-the-badge) 8 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/fakoua/ink/Deno%20CI?style=for-the-badge) 9 | 10 | ## Run the welcome page for interactive demo: 11 | 12 | ```bash 13 | deno run -A https://deno.land/x/ink/welcome.ts 14 | ``` 15 | 16 | ## Examples 17 | 18 | ```ts 19 | import * as ink from 'https://deno.land/x/ink/mod.ts' 20 | 21 | let text = ink.colorize('Hello World') 22 | console.log(text) 23 | ``` 24 | 25 | Output: 26 | 27 | ```diff 28 | - Hello World 29 | ``` 30 | 31 | You can use nested style: 32 | 33 | ```ts 34 | import * as ink from 'https://deno.land/x/ink/mod.ts' 35 | 36 | let text = ink.colorize('Hello World') 37 | 38 | console.log(text) 39 | ``` 40 | 41 | ## Support tags [Simple Mode] 42 | 43 | - <b>: bold 44 | - <i>: italic 45 | - <u>: underline 46 | - <s>: strikethrough 47 | - <hidden>: hidden text 48 | - <inv>: inverted color 49 | - <dim>: dim light 50 | - <u>: underline 51 | 52 | - <red>: text red 53 | - <green>: text green 54 | - <blue>: text blue 55 | - <yellow>: text yellow 56 | - <magenta>: text magenta 57 | - <cyan>: text cyan 58 | - <white>: text white 59 | - <black>: text black 60 | 61 | - <bg-red>: background red 62 | - <bg-green>: background green 63 | - <bg-blue>: background blue 64 | - <bg-yellow>: background yellow 65 | - <bg-magenta>: background magenta 66 | - <bg-cyan>: background cyan 67 | - <bg-white>: background white 68 | - <bg-black>: background black 69 | 70 | ## Alias to console 71 | 72 | You can use the object terminal to call console.log, console.trace ... directly form ink module. 73 | 74 | ```ts 75 | import * as ink from 'https://deno.land/x/ink/mod.ts' 76 | 77 | ink.terminal.log('Hello %s', 'World') 78 | ``` 79 | 80 | ## Advanced mode 81 | 82 | You can use html like style for advanced and nested mode using the `ink` tag: 83 | 84 | ```ts 85 | import * as ink from 'https://deno.land/x/ink/mod.ts' 86 | 87 | let result = ink.html("Hello World") 88 | console.log(result); 89 | ``` 90 | 91 | **ink** also supports nested styles: 92 | 93 | ```ts 94 | let html = ` 95 | 96 | I'm Red, background Green, underlined and bold! 97 | 98 | My BG is black again, but I'm italic :( 99 | 100 | My BG is Green Again! 101 | 102 | No Format here 103 | ` 104 | 105 | let result = ink.html(html) 106 | console.log(result); 107 | ``` 108 | 109 | Output: 110 | 111 | ![output](forgit/img01.png) 112 | 113 | ## String Extension 114 | 115 | `ink` also supports string extension: 116 | 117 | ```ts 118 | import "https://deno.land/x/ink/mod.ts" //Import .toColor() extension 119 | 120 | console.log("Hello Deno".toColor()) 121 | ``` 122 | 123 | ### Supported Styles 124 | 125 | - **color**: Hex Or RGB [#ff0000, rgb(0, 255, 0) ...] 126 | - **background-color**: Hex Or RGB [#ff0000, rgb(0, 255, 0) ...] 127 | - **font**: comma separated values [bold, dim, italic, underline, inverse, hidden, strikethrough] 128 | 129 | ## Draw Image 130 | 131 | Ink module also can draw a JPEG image from local or remote source into the terminal: 132 | 133 | ```ts 134 | import * as ink from "./mod.ts" 135 | 136 | await ink.drawImage("https://placekitten.com/50/50") 137 | 138 | ``` 139 | 140 | ![output](forgit/img02.png) 141 | 142 | ## License 143 | 144 | [MIT](LICENSE) 145 | 146 | 147 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ffakoua%2Fink.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Ffakoua%2Fink?ref=badge_large) 148 | -------------------------------------------------------------------------------- /welcome.ts: -------------------------------------------------------------------------------- 1 | import "./mod.ts" 2 | import * as ink from "./mod.ts" 3 | 4 | 5 | function waitConsole(msg: string) { 6 | ink.terminal.log(`${msg}`) 7 | // let p = new Uint8Array(100); 8 | // Deno.readSync(Deno.stdin.rid, p) 9 | // console.log('\x1b[2J'); 10 | } 11 | 12 | function writeInfo(msg: string) { 13 | ink.terminal.log(`${msg} \r\n`) 14 | } 15 | 16 | console.log("\x1b[2J"); 17 | console.log("\n\r"); 18 | ink.terminal.log("[ Welcome to ink sandbox preview. ]"); 19 | ink.terminal.log("Ink is an advanced terminal tool for string coloring and format.") 20 | waitConsole("Press ENTER to start ...") 21 | 22 | writeInfo("1- Basic Mode:"); 23 | ink.terminal.log(" //import the module") 24 | ink.terminal.log(" import * as ink from 'https://deno.land/x/ink/mod.ts'") 25 | ink.terminal.log("\r\n let text = ink.colorize('<red>Hello World</red>')") 26 | ink.terminal.log(" console.log(text)\r\n") 27 | ink.terminal.log("Output:") 28 | ink.terminal.log("> Hello World\r\n___________________") 29 | waitConsole("Press Enter to continue ...") 30 | 31 | writeInfo("* You can use nested styles:") 32 | ink.terminal.log(" import * as ink from 'https://deno.land/x/ink/mod.ts'") 33 | ink.terminal.log("\r\n let text = ink.colorize('<bg-blue><red>Hello World</bg-blue></red>')") 34 | ink.terminal.log(" console.log(text)\r\n___________________") 35 | ink.terminal.log("Output:") 36 | ink.terminal.log("> Hello World") 37 | 38 | waitConsole("Press Enter to continue ...") 39 | writeInfo("* Supported TAGs are:") 40 | ink.list() 41 | 42 | waitConsole("Press Enter to continue ...") 43 | writeInfo("2- Alias to console object:") 44 | ink.terminal.log(" import * as ink from 'https://deno.land/x/ink/mod.ts'") 45 | ink.terminal.log(" ink.terminal.log('<red>Hello</red> %s', '<b>World</b>')") 46 | ink.terminal.log("Output:") 47 | ink.terminal.log("> Hello %s", "World") 48 | 49 | waitConsole("Press Enter to continue ...") 50 | writeInfo("3- Advanced mode:") 51 | ink.terminal.log("You can use html like style for advanced and nested mode using the ink tag:") 52 | ink.terminal.log(" import * as ink from 'https://deno.land/x/ink/mod.ts'") 53 | ink.terminal.log(` let result = ink.html("<ink style='color: #ff0000;font:bold;'>Hello World</ink>")`) 54 | ink.terminal.log(" console.log(result);") 55 | ink.terminal.log("Output:") 56 | const result = ink.html("> Hello World") 57 | console.log(result); 58 | waitConsole("Press Enter to continue ...") 59 | writeInfo("* ink also supports nested styles:") 60 | const output = 61 | " let html = \"<ink style=\"color: rgb(255, 0, 0); background-color: #00ff00;font: underline, bold\">" + "\r\n" + 62 | " I am Red, background Green, underlined and bold! " + "\r\n" + 63 | " <ink style=\"background-color: rgb(0, 0, 0); font: italic;\">" + "\r\n" + 64 | " My BG is black again, but I am italic :(" + "\r\n" + 65 | " </ink>" + "\r\n" + 66 | " My BG is Green Again!" + "\r\n" + 67 | " </ink>" + "\r\n" + 68 | " No Format here\"" + "\r\n" + 69 | " let result = ink.html(html);" + "\r\n" + 70 | " console.log(result);" + "\r\n" 71 | 72 | ink.terminal.log(output); 73 | ink.terminal.log("Output:") 74 | 75 | const html = 76 | "" + "\r\n" + 77 | " I am Red, background Green, underlined and bold! " + "\r\n" + 78 | " " + "\r\n" + 79 | " My BG is black again, but I am italic :(" + "\r\n" + 80 | " " + "\r\n" + 81 | " My BG is Green Again!" + "\r\n" + 82 | " \r\n No Format here" 83 | ink.terminal.log(html.toColor() + "\r\n_______________"); 84 | 85 | waitConsole("Press Enter to continue ...") 86 | writeInfo("* ink also supports string extension:") 87 | ink.terminal.log(" import \"https://deno.land/x/ink/mod.ts\" //Import .toColor() extension") 88 | ink.terminal.log(` console.log("<ink style='color: #ff0000; font: bold'>Hello Deno</ink>".toColor())`) 89 | ink.terminal.log("Output:") 90 | console.log("> Hello Deno".toColor()) 91 | console.log("______________________________") 92 | waitConsole("Press Enter to continue ...") 93 | writeInfo("* Supported Styles: ") 94 | ink.terminal.log(" * color: Hex Or RGB [#ff0000, rgb(0, 255, 0) ...]") 95 | ink.terminal.log(" * background-color: Hex Or RGB [#ff0000, rgb(0, 255, 0) ...]") 96 | ink.terminal.log(" * font: comma separated values [bold, dim, italic, underline, inverse, hidden, strikethrough]") 97 | console.log("______________________________") 98 | 99 | waitConsole("Press Enter to continue ...") 100 | writeInfo("4 Draw Image: ") 101 | ink.terminal.log("Ink module also can draw a JPEG image from local or remote source into the terminal") 102 | ink.terminal.log(" await ink.drawImage(\"https://placekitten.com/50/50\")") 103 | ink.terminal.log("Output:") 104 | await ink.drawImage("https://placekitten.com/50/50") 105 | 106 | waitConsole("Press Enter to continue ...") 107 | ink.terminal.log("For more information please visit: https://github.com/fakoua/ink") 108 | console.log("\n\r"); 109 | -------------------------------------------------------------------------------- /tsdom/tsdom.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | function decode(input: string): string { 4 | return input; 5 | } 6 | 7 | export enum NodeType { 8 | ELEMENT_NODE = 1, 9 | TEXT_NODE = 3 10 | } 11 | 12 | /** 13 | * Node Class as base class for TextNode and HTMLElement. 14 | */ 15 | export abstract class Node { 16 | //@ts-ignore 17 | nodeType: NodeType; 18 | childNodes = [] as Node[]; 19 | //@ts-ignore 20 | text: string; 21 | //@ts-ignore 22 | rawText: string; 23 | abstract toString(): String; 24 | } 25 | /** 26 | * TextNode to contain a text element in DOM tree. 27 | * @param {string} value [description] 28 | */ 29 | export class TextNode extends Node { 30 | constructor(value: string) { 31 | super(); 32 | this.rawText = value; 33 | } 34 | 35 | /** 36 | * Node Type declaration. 37 | * @type {Number} 38 | */ 39 | nodeType = NodeType.TEXT_NODE; 40 | 41 | /** 42 | * Get unescaped text value of current node and its children. 43 | * @return {string} text content 44 | */ 45 | //@ts-ignore 46 | get text() { 47 | return decode(this.rawText); 48 | } 49 | 50 | /** 51 | * Detect if the node contains only white space. 52 | * @return {bool} 53 | */ 54 | get isWhitespace() { 55 | return /^(\s| )*$/.test(this.rawText); 56 | } 57 | 58 | toString() { 59 | return this.text; 60 | } 61 | } 62 | 63 | const kBlockElements = { 64 | div: true, 65 | p: true, 66 | // ul: true, 67 | // ol: true, 68 | li: true, 69 | // table: true, 70 | // tr: true, 71 | td: true, 72 | section: true, 73 | br: true 74 | }; 75 | 76 | export interface KeyAttributes { 77 | id?: string; 78 | class?: string; 79 | } 80 | 81 | export interface Attributes { 82 | [key: string]: string; 83 | } 84 | 85 | export interface RawAttributes { 86 | [key: string]: string; 87 | } 88 | 89 | function arr_back(arr: T[]) { 90 | return arr[arr.length - 1]; 91 | } 92 | 93 | /** 94 | * HTMLElement, which contains a set of children. 95 | * 96 | * Note: this is a minimalist implementation, no complete tree 97 | * structure provided (no parentNode, nextSibling, 98 | * previousSibling etc). 99 | * @class HTMLElement 100 | * @extends {Node} 101 | */ 102 | export class HTMLElement extends Node { 103 | //@ts-ignore 104 | private _attrs: Attributes; 105 | //@ts-ignore 106 | private _rawAttrs: RawAttributes; 107 | //@ts-ignore 108 | public id: string; 109 | public classNames = [] as string[]; 110 | /** 111 | * Node Type declaration. 112 | */ 113 | public nodeType = NodeType.ELEMENT_NODE; 114 | /** 115 | * Creates an instance of HTMLElement. 116 | * @param keyAttrs id and class attribute 117 | * @param [rawAttrs] attributes in string 118 | * 119 | * @memberof HTMLElement 120 | */ 121 | //@ts-ignore 122 | constructor(public tagName: string, keyAttrs: KeyAttributes, private rawAttrs = '', public parentNode = null as Node) { 123 | super(); 124 | this.rawAttrs = rawAttrs || ''; 125 | this.parentNode = parentNode || null; 126 | this.childNodes = []; 127 | if (keyAttrs.id) { 128 | this.id = keyAttrs.id; 129 | } 130 | if (keyAttrs.class) { 131 | this.classNames = keyAttrs.class.split(/\s+/); 132 | } 133 | } 134 | /** 135 | * Remove Child element from childNodes array 136 | * @param {HTMLElement} node node to remove 137 | */ 138 | public removeChild(node: Node) { 139 | this.childNodes = this.childNodes.filter((child) => { 140 | return (child !== node); 141 | }); 142 | } 143 | /** 144 | * Exchanges given child with new child 145 | * @param {HTMLElement} oldNode node to exchange 146 | * @param {HTMLElement} newNode new node 147 | */ 148 | public exchangeChild(oldNode: Node, newNode: Node) { 149 | let idx = -1; 150 | for (let i = 0; i < this.childNodes.length; i++) { 151 | if (this.childNodes[i] === oldNode) { 152 | idx = i; 153 | break; 154 | } 155 | } 156 | this.childNodes[idx] = newNode; 157 | } 158 | /** 159 | * Get escpaed (as-it) text value of current node and its children. 160 | * @return {string} text content 161 | */ 162 | //@ts-ignore 163 | get rawText() { 164 | let res = ''; 165 | for (let i = 0; i < this.childNodes.length; i++) 166 | res += this.childNodes[i].rawText; 167 | return res; 168 | } 169 | /** 170 | * Get unescaped text value of current node and its children. 171 | * @return {string} text content 172 | */ 173 | //@ts-ignore 174 | get text() { 175 | return decode(this.rawText); 176 | } 177 | /** 178 | * Get structured Text (with '\n' etc.) 179 | * @return {string} structured text 180 | */ 181 | get structuredText() { 182 | let currentBlock = [] as string[]; 183 | const blocks = [currentBlock]; 184 | function dfs(node: Node) { 185 | if (node.nodeType === NodeType.ELEMENT_NODE) { 186 | //@ts-ignore 187 | if (kBlockElements[(node as HTMLElement).tagName]) { 188 | if (currentBlock.length > 0) { 189 | blocks.push(currentBlock = []); 190 | } 191 | node.childNodes.forEach(dfs); 192 | if (currentBlock.length > 0) { 193 | blocks.push(currentBlock = []); 194 | } 195 | } else { 196 | node.childNodes.forEach(dfs); 197 | } 198 | } else if (node.nodeType === NodeType.TEXT_NODE) { 199 | if ((node as TextNode).isWhitespace) { 200 | // Whitespace node, postponed output 201 | (currentBlock as any).prependWhitespace = true; 202 | } else { 203 | let text = node.text; 204 | if ((currentBlock as any).prependWhitespace) { 205 | text = ' ' + text; 206 | (currentBlock as any).prependWhitespace = false; 207 | } 208 | currentBlock.push(text); 209 | } 210 | } 211 | } 212 | dfs(this); 213 | return blocks 214 | .map(function (block) { 215 | // Normalize each line's whitespace 216 | return block.join('').trim().replace(/\s{2,}/g, ' '); 217 | }) 218 | .join('\n').replace(/\s+$/, ''); // trimRight; 219 | } 220 | 221 | public toString() { 222 | const tag = this.tagName; 223 | if (tag) { 224 | const is_un_closed = /^meta$/i.test(tag); 225 | const is_self_closed = /^(img|br|hr|area|base|input|doctype|link)$/i.test(tag); 226 | const attrs = this.rawAttrs ? ' ' + this.rawAttrs : ''; 227 | if (is_un_closed) { 228 | return `<${tag}${attrs}>`; 229 | } else if (is_self_closed) { 230 | return `<${tag}${attrs} />`; 231 | } else { 232 | return `<${tag}${attrs}>${this.innerHTML}`; 233 | } 234 | } else { 235 | return this.innerHTML; 236 | } 237 | } 238 | 239 | get innerHTML() { 240 | return this.childNodes.map((child) => { 241 | return child.toString(); 242 | }).join(''); 243 | } 244 | 245 | public set_content(content: string | Node | Node[]) { 246 | if (content instanceof Node) { 247 | content = [content]; 248 | } else if (typeof content == 'string') { 249 | const r = parse(content); 250 | content = r.childNodes.length ? r.childNodes : [new TextNode(content)]; 251 | } 252 | this.childNodes = content as Node[]; 253 | } 254 | 255 | get outerHTML() { 256 | return this.toString(); 257 | } 258 | 259 | /** 260 | * Trim element from right (in block) after seeing pattern in a TextNode. 261 | * @param {RegExp} pattern pattern to find 262 | * @return {HTMLElement} reference to current node 263 | */ 264 | public trimRight(pattern: RegExp) { 265 | for (let i = 0; i < this.childNodes.length; i++) { 266 | const childNode = this.childNodes[i]; 267 | if (childNode.nodeType === NodeType.ELEMENT_NODE) { 268 | (childNode as HTMLElement).trimRight(pattern); 269 | } else { 270 | const index = childNode.rawText.search(pattern); 271 | if (index > -1) { 272 | childNode.rawText = childNode.rawText.substr(0, index); 273 | // trim all following nodes. 274 | this.childNodes.length = i + 1; 275 | } 276 | } 277 | } 278 | return this; 279 | } 280 | /** 281 | * Get DOM structure 282 | * @return {string} strucutre 283 | */ 284 | get structure() { 285 | const res = [] as string[]; 286 | let indention = 0; 287 | function write(str: string) { 288 | res.push(' '.repeat(indention) + str); 289 | } 290 | function dfs(node: HTMLElement) { 291 | const idStr = node.id ? ('#' + node.id) : ''; 292 | const classStr = node.classNames.length ? ('.' + node.classNames.join('.')) : ''; 293 | write(node.tagName + idStr + classStr); 294 | indention++; 295 | for (let i = 0; i < node.childNodes.length; i++) { 296 | const childNode = node.childNodes[i]; 297 | if (childNode.nodeType === NodeType.ELEMENT_NODE) { 298 | dfs(childNode as HTMLElement); 299 | } else if (childNode.nodeType === NodeType.TEXT_NODE) { 300 | if (!(childNode as TextNode).isWhitespace) 301 | write('#text'); 302 | } 303 | } 304 | indention--; 305 | } 306 | dfs(this); 307 | return res.join('\n'); 308 | } 309 | 310 | /** 311 | * Remove whitespaces in this sub tree. 312 | * @return {HTMLElement} pointer to this 313 | */ 314 | public removeWhitespace() { 315 | let o = 0; 316 | for (let i = 0; i < this.childNodes.length; i++) { 317 | const node = this.childNodes[i]; 318 | if (node.nodeType === NodeType.TEXT_NODE) { 319 | if ((node as TextNode).isWhitespace) 320 | continue; 321 | node.rawText = node.rawText.trim(); 322 | } else if (node.nodeType === NodeType.ELEMENT_NODE) { 323 | (node as HTMLElement).removeWhitespace(); 324 | } 325 | this.childNodes[o++] = node; 326 | } 327 | this.childNodes.length = o; 328 | return this; 329 | } 330 | 331 | /** 332 | * Query CSS selector to find matching nodes. 333 | * @param {string} selector Simplified CSS selector 334 | * @param {Matcher} selector A Matcher instance 335 | * @return {HTMLElement[]} matching elements 336 | */ 337 | public querySelectorAll(selector: string | Matcher) { 338 | let matcher: Matcher; 339 | if (selector instanceof Matcher) { 340 | matcher = selector; 341 | matcher.reset(); 342 | } else { 343 | matcher = new Matcher(selector); 344 | } 345 | const res = [] as HTMLElement[]; 346 | const stack = [] as { 0: Node; 1: 0 | 1; 2: boolean; }[]; 347 | for (let i = 0; i < this.childNodes.length; i++) { 348 | stack.push([this.childNodes[i], 0, false]); 349 | while (stack.length) { 350 | const state = arr_back(stack); 351 | const el = state[0]; 352 | if (state[1] === 0) { 353 | // Seen for first time. 354 | if (el.nodeType !== NodeType.ELEMENT_NODE) { 355 | stack.pop(); 356 | continue; 357 | } 358 | if (state[2] = matcher.advance(el)) { 359 | if (matcher.matched) { 360 | res.push(el as HTMLElement); 361 | // no need to go further. 362 | matcher.rewind(); 363 | stack.pop(); 364 | continue; 365 | } 366 | } 367 | } 368 | if (state[1] < el.childNodes.length) { 369 | stack.push([el.childNodes[state[1]++], 0, false]); 370 | } else { 371 | if (state[2]) 372 | matcher.rewind(); 373 | stack.pop(); 374 | } 375 | } 376 | } 377 | return res; 378 | } 379 | 380 | /** 381 | * Query CSS Selector to find matching node. 382 | * @param {string} selector Simplified CSS selector 383 | * @param {Matcher} selector A Matcher instance 384 | * @return {HTMLElement} matching node 385 | */ 386 | public querySelector(selector: string | Matcher) { 387 | let matcher: Matcher; 388 | if (selector instanceof Matcher) { 389 | matcher = selector; 390 | matcher.reset(); 391 | } else { 392 | matcher = new Matcher(selector); 393 | } 394 | const stack = [] as { 0: Node; 1: 0 | 1; 2: boolean; }[]; 395 | for (let i = 0; i < this.childNodes.length; i++) { 396 | stack.push([this.childNodes[i], 0, false]); 397 | while (stack.length) { 398 | const state = arr_back(stack); 399 | const el = state[0]; 400 | if (state[1] === 0) { 401 | // Seen for first time. 402 | if (el.nodeType !== NodeType.ELEMENT_NODE) { 403 | stack.pop(); 404 | continue; 405 | } 406 | if (state[2] = matcher.advance(el)) { 407 | if (matcher.matched) { 408 | return el as HTMLElement; 409 | } 410 | } 411 | } 412 | if (state[1] < el.childNodes.length) { 413 | stack.push([el.childNodes[state[1]++], 0, false]); 414 | } else { 415 | if (state[2]) 416 | matcher.rewind(); 417 | stack.pop(); 418 | } 419 | } 420 | } 421 | return null; 422 | } 423 | 424 | /** 425 | * Append a child node to childNodes 426 | * @param {Node} node node to append 427 | * @return {Node} node appended 428 | */ 429 | public appendChild(node: T) { 430 | // node.parentNode = this; 431 | this.childNodes.push(node); 432 | if (node instanceof HTMLElement) { 433 | node.parentNode = this; 434 | } 435 | return node; 436 | } 437 | 438 | /** 439 | * Get first child node 440 | * @return {Node} first child node 441 | */ 442 | get firstChild() { 443 | return this.childNodes[0]; 444 | } 445 | 446 | /** 447 | * Get last child node 448 | * @return {Node} last child node 449 | */ 450 | get lastChild() { 451 | return arr_back(this.childNodes); 452 | } 453 | 454 | /** 455 | * Get attributes 456 | * @return {Object} parsed and unescaped attributes 457 | */ 458 | get attributes() { 459 | if (this._attrs) 460 | return this._attrs; 461 | this._attrs = {}; 462 | const attrs = this.rawAttributes; 463 | for (const key in attrs) { 464 | this._attrs[key] = decode(attrs[key]); 465 | } 466 | return this._attrs; 467 | } 468 | 469 | /** 470 | * Get escaped (as-it) attributes 471 | * @return {Object} parsed attributes 472 | */ 473 | get rawAttributes() { 474 | if (this._rawAttrs) 475 | return this._rawAttrs; 476 | const attrs = {} as RawAttributes; 477 | if (this.rawAttrs) { 478 | const re = /\b([a-z][a-z0-9\-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/ig; 479 | let match: RegExpExecArray; 480 | //@ts-ignore 481 | while (match = re.exec(this.rawAttrs)) { 482 | attrs[match[1]] = match[2] || match[3] || match[4] || ""; 483 | } 484 | } 485 | this._rawAttrs = attrs; 486 | return attrs; 487 | } 488 | } 489 | 490 | interface MatherFunction { func: any; tagName: string; classes: string | string[]; attr_key: any; value: any; } 491 | 492 | /** 493 | * Cache to store generated match functions 494 | * @type {Object} 495 | */ 496 | let pMatchFunctionCache = {} as { [name: string]: MatherFunction }; 497 | 498 | /** 499 | * Function cache 500 | */ 501 | const functionCache = { 502 | "f145": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 503 | "use strict"; 504 | tagName = tagName || ""; 505 | classes = classes || []; 506 | attr_key = attr_key || ""; 507 | value = value || ""; 508 | if (el.id != tagName.substr(1)) return false; 509 | for (let cls = classes, i = 0; i < cls.length; i++) if (el.classNames.indexOf(cls[i]) === -1) return false; 510 | return true; 511 | }, 512 | "f45": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 513 | "use strict"; 514 | tagName = tagName || ""; 515 | classes = classes || []; 516 | attr_key = attr_key || ""; 517 | value = value || ""; 518 | for (let cls = classes, i = 0; i < cls.length; i++) if (el.classNames.indexOf(cls[i]) === -1) return false; 519 | return true; 520 | }, 521 | "f15": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 522 | "use strict"; 523 | tagName = tagName || ""; 524 | classes = classes || []; 525 | attr_key = attr_key || ""; 526 | value = value || ""; 527 | if (el.id != tagName.substr(1)) return false; 528 | return true; 529 | }, 530 | "f1": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 531 | "use strict"; 532 | tagName = tagName || ""; 533 | classes = classes || []; 534 | attr_key = attr_key || ""; 535 | value = value || ""; 536 | if (el.id != tagName.substr(1)) return false; 537 | }, 538 | "f5": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 539 | "use strict"; 540 | el = el || {} as HTMLElement; 541 | tagName = tagName || ""; 542 | classes = classes || []; 543 | attr_key = attr_key || ""; 544 | value = value || ""; 545 | return true; 546 | }, 547 | "f245": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 548 | "use strict"; 549 | tagName = tagName || ""; 550 | classes = classes || []; 551 | attr_key = attr_key || ""; 552 | value = value || ""; 553 | let attrs = el.attributes; for (let key in attrs) { const val = attrs[key]; if (key == attr_key && val == value) { return true; } } return false; 554 | // for (let cls = classes, i = 0; i < cls.length; i++) {if (el.classNames.indexOf(cls[i]) === -1){ return false;}} 555 | // return true; 556 | }, 557 | "f25": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 558 | "use strict"; 559 | tagName = tagName || ""; 560 | classes = classes || []; 561 | attr_key = attr_key || ""; 562 | value = value || ""; 563 | let attrs = el.attributes; for (let key in attrs) { const val = attrs[key]; if (key == attr_key && val == value) { return true; } } return false; 564 | //return true; 565 | }, 566 | "f2": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 567 | "use strict"; 568 | tagName = tagName || ""; 569 | classes = classes || []; 570 | attr_key = attr_key || ""; 571 | value = value || ""; 572 | let attrs = el.attributes; for (let key in attrs) { const val = attrs[key]; if (key == attr_key && val == value) { return true; } } return false; 573 | }, 574 | "f345": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 575 | "use strict"; 576 | tagName = tagName || ""; 577 | classes = classes || []; 578 | attr_key = attr_key || ""; 579 | value = value || ""; 580 | if (el.tagName != tagName) return false; 581 | for (let cls = classes, i = 0; i < cls.length; i++) if (el.classNames.indexOf(cls[i]) === -1) return false; 582 | return true; 583 | }, 584 | "f35": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 585 | "use strict"; 586 | tagName = tagName || ""; 587 | classes = classes || []; 588 | attr_key = attr_key || ""; 589 | value = value || ""; 590 | if (el.tagName != tagName) return false; 591 | return true; 592 | }, 593 | "f3": function (el: HTMLElement, tagName: string, classes: string[], attr_key: string, value: string) { 594 | "use strict"; 595 | tagName = tagName || ""; 596 | classes = classes || []; 597 | attr_key = attr_key || ""; 598 | value = value || ""; 599 | if (el.tagName != tagName) return false; 600 | } 601 | } 602 | /** 603 | * Matcher class to make CSS match 604 | * 605 | * @class Matcher 606 | */ 607 | export class Matcher { 608 | private matchers: MatherFunction[]; 609 | private nextMatch = 0; 610 | /** 611 | * Creates an instance of Matcher. 612 | * @param {string} selector 613 | * 614 | * @memberof Matcher 615 | */ 616 | constructor(selector: string) { 617 | functionCache["f5"] = functionCache["f5"]; 618 | this.matchers = selector.split(' ').map((matcher) => { 619 | if (pMatchFunctionCache[matcher]) 620 | return pMatchFunctionCache[matcher]; 621 | const parts = matcher.split('.'); 622 | const tagName = parts[0]; 623 | const classes = parts.slice(1).sort(); 624 | let source = '"use strict";'; 625 | let function_name = 'f'; 626 | let attr_key = ""; 627 | let value = ""; 628 | if (tagName && tagName != '*') { 629 | let matcher: RegExpMatchArray; 630 | if (tagName[0] == '#') { 631 | source += 'if (el.id != ' + JSON.stringify(tagName.substr(1)) + ') return false;';//1 632 | function_name += '1'; 633 | } 634 | //@ts-ignore 635 | else if (matcher = tagName.match(/^\[\s*(\S+)\s*(=|!=)\s*((((["'])([^\6]*)\6))|(\S*?))\]\s*/)) { 636 | attr_key = matcher[1]; 637 | let method = matcher[2]; 638 | if (method !== '=' && method !== '!=') { 639 | throw new Error('Selector not supported, Expect [key${op}value].op must be =,!='); 640 | } 641 | if (method === '=') { 642 | method = '=='; 643 | } 644 | value = matcher[7] || matcher[8]; 645 | 646 | source += `let attrs = el.attributes;for (let key in attrs){const val = attrs[key]; if (key == "${attr_key}" && val == "${value}"){return true;}} return false;`;//2 647 | function_name += '2'; 648 | } else { 649 | source += 'if (el.tagName != ' + JSON.stringify(tagName) + ') return false;';//3 650 | function_name += '3'; 651 | } 652 | } 653 | if (classes.length > 0) { 654 | source += 'for (let cls = ' + JSON.stringify(classes) + ', i = 0; i < cls.length; i++) if (el.classNames.indexOf(cls[i]) === -1) return false;';//4 655 | function_name += '4'; 656 | } 657 | source += 'return true;';//5 658 | function_name += '5'; 659 | let obj = { 660 | //@ts-ignore 661 | func: functionCache[function_name], 662 | tagName: tagName || "", 663 | classes: classes || "", 664 | attr_key: attr_key || "", 665 | value: value || "" 666 | } 667 | source = source || ""; 668 | return pMatchFunctionCache[matcher] = obj as MatherFunction; 669 | }); 670 | } 671 | /** 672 | * Trying to advance match pointer 673 | * @param {HTMLElement} el element to make the match 674 | * @return {bool} true when pointer advanced. 675 | */ 676 | advance(el: Node) { 677 | if (this.nextMatch < this.matchers.length && 678 | this.matchers[this.nextMatch].func(el, this.matchers[this.nextMatch].tagName, this.matchers[this.nextMatch].classes, this.matchers[this.nextMatch].attr_key, this.matchers[this.nextMatch].value)) { 679 | this.nextMatch++; 680 | return true; 681 | } 682 | return false; 683 | } 684 | /** 685 | * Rewind the match pointer 686 | */ 687 | rewind() { 688 | this.nextMatch--; 689 | } 690 | /** 691 | * Trying to determine if match made. 692 | * @return {bool} true when the match is made 693 | */ 694 | get matched() { 695 | return this.nextMatch == this.matchers.length; 696 | } 697 | /** 698 | * Rest match pointer. 699 | * @return {[type]} [description] 700 | */ 701 | reset() { 702 | this.nextMatch = 0; 703 | } 704 | /** 705 | * flush cache to free memory 706 | */ 707 | flushCache() { 708 | pMatchFunctionCache = {}; 709 | } 710 | } 711 | 712 | // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name 713 | const kMarkupPattern = /)-->|<(\/?)([a-z][-.0-9_a-z]*)\s*([^>]*?)(\/?)>/ig; 714 | const kAttributePattern = /(^|\s)(id|class)\s*=\s*("([^"]+)"|'([^']+)'|(\S+))/ig; 715 | const kSelfClosingElements = { 716 | area: true, 717 | base: true, 718 | br: true, 719 | col: true, 720 | hr: true, 721 | img: true, 722 | input: true, 723 | link: true, 724 | meta: true, 725 | source: true 726 | }; 727 | const kElementsClosedByOpening = { 728 | li: { li: true }, 729 | p: { p: true, div: true }, 730 | b: { div: true }, 731 | td: { td: true, th: true }, 732 | th: { td: true, th: true }, 733 | h1: { h1: true }, 734 | h2: { h2: true }, 735 | h3: { h3: true }, 736 | h4: { h4: true }, 737 | h5: { h5: true }, 738 | h6: { h6: true } 739 | }; 740 | const kElementsClosedByClosing = { 741 | li: { ul: true, ol: true }, 742 | a: { div: true }, 743 | b: { div: true }, 744 | i: { div: true }, 745 | p: { div: true }, 746 | td: { tr: true, table: true }, 747 | th: { tr: true, table: true } 748 | }; 749 | const kBlockTextElements = { 750 | script: true, 751 | noscript: true, 752 | style: true, 753 | pre: true 754 | }; 755 | 756 | /** 757 | * Parses HTML and returns a root element 758 | * Parse a chuck of HTML source. 759 | * @param {string} data html 760 | * @return {HTMLElement} root element 761 | */ 762 | export function parse(data: string, options?: { 763 | lowerCaseTagName?: boolean; 764 | noFix?: boolean; 765 | script?: boolean; 766 | style?: boolean; 767 | pre?: boolean; 768 | }) { 769 | //@ts-ignore 770 | const root = new HTMLElement(null, {}); 771 | let currentParent = root; 772 | const stack = [root]; 773 | let lastTextPos = -1; 774 | options = options || {} as any; 775 | let match: RegExpExecArray; 776 | //@ts-ignore 777 | while (match = kMarkupPattern.exec(data)) { 778 | if (lastTextPos > -1) { 779 | if (lastTextPos + match[0].length < kMarkupPattern.lastIndex) { 780 | // if has content 781 | const text = data.substring(lastTextPos, kMarkupPattern.lastIndex - match[0].length); 782 | currentParent.appendChild(new TextNode(text)); 783 | } 784 | } 785 | lastTextPos = kMarkupPattern.lastIndex; 786 | if (match[0][1] == '!') { 787 | // this is a comment 788 | continue; 789 | } 790 | //@ts-ignore 791 | if (options.lowerCaseTagName) 792 | match[2] = match[2].toLowerCase(); 793 | if (!match[1]) { 794 | // not or ... 815 | let closeMarkup = ''; 816 | let index = data.indexOf(closeMarkup, kMarkupPattern.lastIndex); 817 | //@ts-ignore 818 | if (options[match[2]]) { 819 | let text: string; 820 | if (index == -1) { 821 | // there is no matching ending for the text element. 822 | text = data.substr(kMarkupPattern.lastIndex); 823 | } else { 824 | text = data.substring(kMarkupPattern.lastIndex, index); 825 | } 826 | if (text.length > 0) { 827 | currentParent.appendChild(new TextNode(text)); 828 | } 829 | } 830 | if (index == -1) { 831 | lastTextPos = kMarkupPattern.lastIndex = data.length + 1; 832 | } else { 833 | lastTextPos = kMarkupPattern.lastIndex = index + closeMarkup.length; 834 | match[1] = 'true'; 835 | } 836 | } 837 | } 838 | if (match[1] || match[4] || 839 | //@ts-ignore 840 | kSelfClosingElements[match[2]]) { 841 | // or
etc. 842 | while (true) { 843 | if (currentParent.tagName == match[2]) { 844 | stack.pop(); 845 | currentParent = arr_back(stack); 846 | break; 847 | } else { 848 | // Trying to close current tag, and move on 849 | //@ts-ignore 850 | if (kElementsClosedByClosing[currentParent.tagName]) { 851 | //@ts-ignore 852 | if (kElementsClosedByClosing[currentParent.tagName][match[2]]) { 853 | stack.pop(); 854 | currentParent = arr_back(stack); 855 | continue; 856 | } 857 | } 858 | // Use aggressive strategy to handle unmatching markups. 859 | break; 860 | } 861 | } 862 | } 863 | } 864 | type Response = (HTMLElement | TextNode) & { valid: boolean; }; 865 | const valid = !!(stack.length === 1); 866 | //@ts-ignore 867 | if (!options.noFix) { 868 | const response = root as Response; 869 | response.valid = valid; 870 | while (stack.length > 1) { 871 | // Handle each error elements. 872 | const last = stack.pop(); 873 | const oneBefore = arr_back(stack); 874 | //@ts-ignore 875 | if (last.parentNode && (last.parentNode as HTMLElement).parentNode) { 876 | //@ts-ignore 877 | if (last.parentNode === oneBefore && last.tagName === oneBefore.tagName) { 878 | // Pair error case

handle : Fixes to

879 | //@ts-ignore 880 | oneBefore.removeChild(last); 881 | //@ts-ignore 882 | last.childNodes.forEach((child) => { 883 | (oneBefore.parentNode as HTMLElement).appendChild(child); 884 | }); 885 | stack.pop(); 886 | } else { 887 | // Single error

handle: Just removes

888 | //@ts-ignore 889 | oneBefore.removeChild(last); 890 | //@ts-ignore 891 | last.childNodes.forEach((child) => { 892 | oneBefore.appendChild(child); 893 | }); 894 | } 895 | } else { 896 | // If it's final element just skip. 897 | } 898 | } 899 | response.childNodes.forEach((node) => { 900 | if (node instanceof HTMLElement) { 901 | //@ts-ignore 902 | node.parentNode = null; 903 | } 904 | }); 905 | return response; 906 | } else { 907 | const response = new TextNode(data) as Response; 908 | response.valid = valid; 909 | return response; 910 | } 911 | } --------------------------------------------------------------------------------