diff --git a/.taprc b/.taprc
new file mode 100644
index 0000000..a9e30ee
--- /dev/null
+++ b/.taprc
@@ -0,0 +1,5 @@
+branches: 95
+functions: 95
+lines: 95
+statements: 95
+ts: true
diff --git a/index.ts b/index.ts
deleted file mode 100644
index f431d49..0000000
--- a/index.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { table } from './utils/common';
-export * from './utils/common';
-
-export type ListItems = Array<{ text: string; depth: number } | string>;
-
-export default class Markdown {
- LINES: string[];
-
- constructor() {
- this.LINES = [];
- }
-
- render() {
- return this.LINES.join('\n');
- }
-
- addLine(data = '\n'): this {
- this.LINES.push(`${data}`);
- return this;
- }
-
- text(data: string | number): this {
- this.addLine(`\n${data}\n`);
- return this;
- }
-
- header(title: string, n = 1): this {
- this.addLine(`${'#'.repeat(n)} ${title}`);
- return this;
- }
-
- image(filepath: string): this {
- const str = `\n\n`;
- this.addLine(str);
- return this;
- }
-
- table(columns = [], labels = [], datarows = []): this {
- this.addLine(table(columns, labels, datarows));
- return this;
- }
-
- list(items: ListItems = [], numbered = false): this {
- if (!Array.isArray(items)) {
- throw new TypeError('List items should be an array of strings');
- }
- for (const item of items) {
- let parsed;
- if (numbered) parsed = `${i + 1}. ${item}`;
- parsed = ` ${item}`;
- this.addLine(parsed);
- }
- return this;
- }
-
- tasks(items: string[] = []): this {
- if (!Array.isArray(items)) {
- throw new TypeError('List items should be an array of strings');
- }
- for (const item of items) {
- this.addLine(`- [ ] ${item}`);
- }
-
- return this;
- }
-}
diff --git a/package-lock.json b/package-lock.json
index a9e7360..3e75404 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,6 @@
"name": "@scdev/declarative-markdown",
"version": "1.0.0-0",
"license": "MIT",
- "dependencies": {
- "@types/tap": "^15.0.5"
- },
"devDependencies": {
"@commitlint/cli": "^8.3.5",
"@commitlint/config-conventional": "^8.3.4",
@@ -787,7 +784,9 @@
"node_modules/@types/node": {
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
- "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ=="
+ "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==",
+ "dev": true,
+ "peer": true
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.1",
@@ -799,14 +798,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/tap": {
- "version": "15.0.5",
- "resolved": "https://registry.npmjs.org/@types/tap/-/tap-15.0.5.tgz",
- "integrity": "sha512-MaP+EgZNFAGRvVjHWv8ldrLvYBn4PnmAlzY7IL3/RPAPkOXdggTSTgLFONbnTpdQTe8+ixYGAySKAm9ESlZm1A==",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/acorn": {
"version": "7.4.1",
"dev": true,
@@ -10702,7 +10693,9 @@
"@types/node": {
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
- "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ=="
+ "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==",
+ "dev": true,
+ "peer": true
},
"@types/normalize-package-data": {
"version": "2.4.1",
@@ -10712,14 +10705,6 @@
"version": "4.0.0",
"dev": true
},
- "@types/tap": {
- "version": "15.0.5",
- "resolved": "https://registry.npmjs.org/@types/tap/-/tap-15.0.5.tgz",
- "integrity": "sha512-MaP+EgZNFAGRvVjHWv8ldrLvYBn4PnmAlzY7IL3/RPAPkOXdggTSTgLFONbnTpdQTe8+ixYGAySKAm9ESlZm1A==",
- "requires": {
- "@types/node": "*"
- }
- },
"acorn": {
"version": "7.4.1",
"dev": true
diff --git a/package.json b/package.json
index 66c1922..9584306 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,9 @@
"description": "A simple and configurable Next.js API handler wrapper that avoids pointless boilerplate",
"main": "index.mjs",
"scripts": {
- "build": "tsc index.ts",
+ "build": "tsc",
"test": "tap --ts",
+ "coverage": "tap --coverage-report=html",
"prepare": "husky install"
},
"keywords": [
@@ -42,8 +43,5 @@
"tap": "^15.0.10",
"ts-node": "^10.2.1",
"typescript": "^4.4.3"
- },
- "dependencies": {
- "@types/tap": "^15.0.5"
}
}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..768a034
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,118 @@
+import { table, LB, CR } from './utils/common';
+export * from './utils/common';
+
+export type ListItems = Array;
+export type ListItem = { text: string; depth?: number };
+export type TaskItem = { text: string; checked?: boolean };
+
+export default class Markdown {
+ LINES: string[];
+
+ constructor(title: string) {
+ this.LINES = [];
+ if (!title) {
+ throw new TypeError('Must Provide a title for this markdown');
+ }
+ this.header(title, 1);
+ }
+
+ tableOfContent(onTop = true) {
+ let list = [];
+ if (!this.LINES.length) return;
+
+ this.header('Table of Contents', 2);
+
+ for (const line of this.LINES) {
+ if (line.startsWith('#')) {
+ const depth = line.split('#').filter((c) => c === '').length - 1;
+ const text = line.match(/^\#+\s?(.*)$/)?.[1];
+ if (!text && !depth) continue;
+ list.push({ text, depth });
+ }
+ }
+
+ this.list(list);
+
+ if (onTop) {
+ const lng = this.LINES.length;
+ const toc = this.LINES.splice(lng - 2, 2);
+ this.LINES.splice(1, 0, ...toc);
+ }
+
+ return this;
+ }
+
+ render() {
+ return this.LINES.join(CR);
+ }
+
+ addLine(data = LB): this {
+ this.LINES.push(`${data}`);
+ return this;
+ }
+
+ paragraph(data: string | number): this {
+ this.addLine(`${data}`);
+ return this;
+ }
+
+ header(title: string, n = 1): this {
+ this.addLine(`${'#'.repeat(n)} ${title}`);
+ return this;
+ }
+
+ image(filepath: string, altText?: string): this {
+ const str = `![${altText || filepath}](${filepath})`;
+ this.addLine(str);
+ return this;
+ }
+
+ table(columns = [], rows = [], fmtFnc?): this {
+ this.addLine(table(columns, rows, fmtFnc));
+ return this;
+ }
+
+ list(items: ListItems = [], numbered = false): this {
+ if (!Array.isArray(items)) {
+ throw new TypeError(
+ 'List items should be an array of { text: string, depth: number}'
+ );
+ }
+
+ let list = [];
+
+ for (let i = 0; i < items.length; i++) {
+ let parsed;
+ const { text, depth } = items[i];
+ if (numbered) parsed = `${i + 1}. ${text}`;
+ else {
+ parsed = `${!depth ? '' : ' '.repeat(depth)}- ${text}`;
+ }
+ list.push(parsed);
+ }
+
+ this.addLine(list.join('\n'));
+
+ return this;
+ }
+
+ tasks(items: ListItems = []): this {
+ if (!Array.isArray(items)) {
+ throw new TypeError(
+ 'List items should be an array of { text: string, checked: boolean }'
+ );
+ }
+
+ let list = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const { text, checked } = items[i];
+ const parsed = `- [${checked ? 'X' : ' '}] ${text}`;
+ list.push(parsed);
+ }
+
+ this.addLine(list.join('\n'));
+
+ return this;
+ }
+}
diff --git a/utils/common.ts b/src/utils/common.ts
similarity index 63%
rename from utils/common.ts
rename to src/utils/common.ts
index ec15b01..766e7d2 100644
--- a/utils/common.ts
+++ b/src/utils/common.ts
@@ -1,11 +1,14 @@
+export const CR = '\n\n';
+export const LB = '\n';
+
export const table = (
headers: string[] = [],
rows: string[] = [],
fmtFnc = (rowValue: any) => rowValue
) => {
let t = '';
- t += `| ${headers.join(' | ')} |\n|${' --- |'.repeat(headers.length)}\n`;
- t += `| ${rows.map(fmtFnc).join(' | ')} |\n`;
+ t += `| ${headers.join(' | ')} |${LB}|${' --- |'.repeat(headers.length)}`;
+ t += `${LB}| ${rows.map(fmtFnc).join(' | ')} |`;
return t;
};
@@ -24,17 +27,21 @@ export const bold = (text = '') => {
};
export const link = (text, url) => {
+ if (!text && !url) return '';
return `[${text}](${url})`;
};
-export const quote = (text) => {
- return `> ${text}`;
+export const inlineCode = (text) => {
+ if (!text) return '';
+ return '`' + text + '`';
};
-export const code = (text, language = '') => {
- return '```' + language + '\n' + text + '\n```';
+export const quote = (text) => {
+ if (!text) return '';
+ return `${LB}> ${text}`;
};
-export const inlineCode = (text) => {
- return '`' + text + '`';
+export const code = (text, language = '') => {
+ if (!text) return '';
+ return CR + '```' + language + LB + text + LB + '```';
};
diff --git a/test/common.ts b/test/common.ts
new file mode 100644
index 0000000..1207b63
--- /dev/null
+++ b/test/common.ts
@@ -0,0 +1,59 @@
+import tap from 'tap';
+import {
+ table,
+ link,
+ code,
+ inlineCode,
+ quote,
+ italic,
+ bold,
+} from '../src/utils/common';
+
+tap.test('italic', (t) => {
+ t.equal(italic('ok'), '*ok*');
+ t.equal(italic(null), '');
+ t.end();
+});
+
+tap.test('bold', (t) => {
+ t.equal(bold('ok'), '**ok**');
+ t.equal(bold(null), '');
+ t.end();
+});
+
+tap.test('quote', (t) => {
+ t.equal(quote('ok'), '\n> ok');
+ t.equal(quote(null), '');
+ t.end();
+});
+
+tap.test('link', (t) => {
+ const l = 'http://google.com';
+ const txt = 'link';
+ t.equal(link(txt, l), `[${txt}](${l})`);
+ t.equal(link(null, null), '');
+ t.end();
+});
+
+tap.test('code', (t) => {
+ const codeBlock = "alert('x')";
+ t.equal(inlineCode(codeBlock), '`' + codeBlock + '`');
+ t.equal(inlineCode(null), '');
+ t.equal(code(codeBlock), '\n\n```\n' + codeBlock + '\n```');
+ t.equal(
+ code(codeBlock, 'javascript'),
+ '\n\n```javascript\n' + codeBlock + '\n```'
+ );
+ t.equal(code(null), '');
+ t.end();
+});
+
+tap.test('table', (t) => {
+ const headers = ['id', 'name'];
+ const rows = ['1', 'Ajeje'];
+ let out = '| id | name |\n';
+ out += '| --- | --- |\n';
+ out += '| 1 | Ajeje |';
+ t.equal(table(headers, rows), out);
+ t.end();
+});
diff --git a/test/fixtures/output.md b/test/fixtures/output.md
new file mode 100644
index 0000000..384fb71
--- /dev/null
+++ b/test/fixtures/output.md
@@ -0,0 +1,66 @@
+# Declarative Markdown Generator
+
+## Table of Contents
+
+- Declarative Markdown Generator
+ - Paragraphs
+ - Table
+ - List
+ - Numbered List
+ - Task List
+ - images
+ - Table of Contents
+
+## Paragraphs
+
+My _Italic_ text and the **bold** one
+
+Let's add a [link](http://google.com), why not a quote:
+
+> I've become death, destructor of worlds
+
+Do you want to see my fancy `alert('x')`, but I've a better example here:
+
+```go
+package main
+ func main(){}
+```
+
+## Table
+
+| id | name |
+| --- | ------ |
+| 1 | Simone |
+
+## List
+
+- list1
+ - nested
+ - nested2
+- list2
+ - nested
+ - nested2
+
+## Numbered List
+
+1. list1
+2. nested
+3. nested2
+4. list2
+5. nested
+6. nested2
+
+## Task List
+
+- [x] list1
+- [ ] nested
+- [ ] nested2
+- [ ] list2
+- [ ] nested
+- [ ] nested2
+
+## images
+
+![http://ajeje.com/image.png](http://ajeje.com/image.png)
+
+![ALTTEXT](http://ajeje.com/image.png)
diff --git a/test/index.ts b/test/index.ts
index e414584..85fda5c 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -1,53 +1,84 @@
+// @ts-n
import tap from 'tap';
-import {
- table,
- link,
- code,
- inlineCode,
- quote,
- italic,
- bold,
-} from '../utils/common';
+import Markdown, { italic, bold, link, quote, inlineCode, code } from '../src';
+import fs from 'fs/promises';
-tap.test('italic', (t) => {
- t.equal(italic('ok'), '*ok*');
- t.end();
-});
+tap.test('Shoud generate Markdown', async (t) => {
+ t.throws(() => {
+ // @ts-ignore
+ return new Markdown();
+ });
-tap.test('bold', (t) => {
- t.equal(bold('ok'), '**ok**');
- t.end();
-});
+ const mkd = new Markdown('Declarative Markdown Generator');
+ t.equal(mkd.render(), '# Declarative Markdown Generator');
-tap.test('quote', (t) => {
- t.equal(quote('ok'), '> ok');
- t.end();
-});
+ mkd
+ .header('Paragraphs', 2)
+ .paragraph(`My ${italic('Italic')} text and the ${bold('bold')} one`)
+ .paragraph(
+ `Let's add a ${link(
+ 'link',
+ 'http://google.com'
+ )}, why not a quote: ${quote("I've become death, destructor of worlds")}`
+ )
+ .paragraph(
+ `Do you want to see my fancy ${inlineCode(
+ "alert('x')"
+ )}, but I've a better example here: ${code(
+ 'package main\n func main(){}',
+ 'go'
+ )}`
+ )
+ .header('Table', 2)
+ .table(['id', 'name'], ['1', 'Simone'])
+ .header('List', 2)
+ .list([
+ { text: 'list1', depth: 0 },
+ { text: 'nested', depth: 1 },
+ { text: 'nested2', depth: 1 },
+ { text: 'list2' },
+ { text: 'nested', depth: 1 },
+ { text: 'nested2', depth: 1 },
+ ])
+ .header('Numbered List', 2)
+ .list(
+ [
+ { text: 'list1' },
+ { text: 'nested' },
+ { text: 'nested2' },
+ { text: 'list2' },
+ { text: 'nested' },
+ { text: 'nested2' },
+ ],
+ true
+ )
+ .header('Task List', 2)
+ .tasks([
+ { text: 'list1', checked: true },
+ { text: 'nested' },
+ { text: 'nested2' },
+ { text: 'list2' },
+ { text: 'nested' },
+ { text: 'nested2' },
+ ])
+ .header('images', 2)
+ .image('http://ajeje.com/image.png')
+ .image('http://ajeje.com/image.png', 'ALTTEXT')
+ .tableOfContent();
-tap.test('link', (t) => {
- const l = 'http://google.com';
- const txt = 'link';
- t.equal(link(txt, link), `[${txt}](${link})`);
- t.end();
-});
+ await fs.writeFile('./test/fixtures/output.md', mkd.render());
-tap.test('code', (t) => {
- const codeBlock = "alert('x')";
- t.equal(inlineCode(codeBlock), '`' + codeBlock + '`');
- t.equal(code(codeBlock), '```\n' + codeBlock + '\n```');
- t.equal(
- code(codeBlock, 'javascript'),
- '```javascript\n' + codeBlock + '\n```'
- );
+ // const fixtures = await fs.readFile('./test/fixtures/output.md', 'utf-8');
+ // t.equal(mkd.render(), fixtures);
t.end();
});
-tap.test('table', (t) => {
- const headers = ['id', 'name'];
- const rows = ['1', 'Ajeje'];
- let out = '| id | name |\n';
- out += '| --- | --- |\n';
- out += '| 1 | Ajeje |\n';
- t.equal(table(headers, rows), out);
+tap.test('Check throws', (t) => {
+ const mkd = new Markdown('Throw test');
+
+ // @ts-ignore
+ t.throws(() => mkd.list('test'));
+ // @ts-ignore
+ t.throws(() => mkd.tasks('test'));
t.end();
});
diff --git a/tsconfig.json b/tsconfig.json
index daa0930..9d1656a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,5 +3,7 @@
"target": "ES6",
"esModuleInterop": true,
"module": "commonjs"
- }
+ },
+ "exclude": ["test/**"],
+ "include": ["src"]
}