/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 = `${tagName}>`
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 | 
6 | 
7 | 
8 | 
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 | 
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 | 
141 |
142 | ## License
143 |
144 | [MIT](LICENSE)
145 |
146 |
147 | [](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}${tag}>`;
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 tags
795 | let attrs = {};
796 | for (let attMatch; attMatch = kAttributePattern.exec(match[3]);) {
797 | //@ts-ignore
798 | attrs[attMatch[2]] = attMatch[4] || attMatch[5] || attMatch[6];
799 | }
800 |
801 | //@ts-ignore
802 | if (!match[4] && kElementsClosedByOpening[currentParent.tagName]) {
803 | //@ts-ignore
804 | if (kElementsClosedByOpening[currentParent.tagName][match[2]]) {
805 | stack.pop();
806 | currentParent = arr_back(stack);
807 | }
808 | }
809 | currentParent = currentParent.appendChild(
810 | new HTMLElement(match[2], attrs, match[3]));
811 | stack.push(currentParent);
812 | //@ts-ignore
813 | if (kBlockTextElements[match[2]]) {
814 | // a little test to find next or ...
815 | let closeMarkup = '' + match[2] + '>';
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 /> 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 | }
--------------------------------------------------------------------------------